Git hosting and a container registry in one Rust binary (axum + Astro)
| 1 | -- irongit schema. Postgres holds identity, permissions and metadata; git |
| 2 | -- objects live on the server's disk and large blobs (LFS, image layers, CLI |
| 3 | -- releases, avatars, backups) live in R2. |
| 4 | |
| 5 | create extension if not exists citext; |
| 6 | create extension if not exists pg_trgm; |
| 7 | |
| 8 | -- Users and organizations share one namespace, so /{name} is unambiguous. |
| 9 | ( |
| 10 | id bigserial primary key, |
| 11 | kind text not null check (kind in ('user', 'org')), |
| 12 | name citext not null unique, |
| 13 | display_name text not null default '', |
| 14 | bio text not null default '', |
| 15 | location text not null default '', |
| 16 | website text not null default '', |
| 17 | -- R2 key of an uploaded avatar; null means the generated identicon. |
| 18 | avatar_key text, |
| 19 | -- Per-account override of the default storage quota, in bytes. |
| 20 | quota_bytes bigint, |
| 21 | created_at timestamptz not null default now, |
| 22 | updated_at timestamptz not null default now |
| 23 | ); |
| 24 | on accounts using gin (name gin_trgm_ops); |
| 25 | |
| 26 | ( |
| 27 | account_id bigint primary key references accounts (id) on delete cascade, |
| 28 | -- Null for accounts that only sign in with Google. |
| 29 | password_hash text, |
| 30 | is_admin boolean not null default false, |
| 31 | suspended_at timestamptz, |
| 32 | -- Whether other people see counts from private repos on the heatmap. |
| 33 | show_private_contributions boolean not null default false, |
| 34 | last_login_at timestamptz |
| 35 | ); |
| 36 | |
| 37 | ( |
| 38 | id bigserial primary key, |
| 39 | user_id bigint not null references users (account_id) on delete cascade, |
| 40 | email citext not null unique, |
| 41 | is_primary boolean not null default false, |
| 42 | verified_at timestamptz, |
| 43 | verify_token_hash text, |
| 44 | verify_sent_at timestamptz, |
| 45 | created_at timestamptz not null default now |
| 46 | ); |
| 47 | on emails (user_id) where is_primary; |
| 48 | on emails (user_id); |
| 49 | |
| 50 | -- External sign-in (Google). `subject` is the provider's stable user id. |
| 51 | ( |
| 52 | provider text not null check (provider in ('google')), |
| 53 | subject text not null, |
| 54 | user_id bigint not null references users (account_id) on delete cascade, |
| 55 | email citext, |
| 56 | created_at timestamptz not null default now, |
| 57 | last_used_at timestamptz, |
| 58 | primary key (provider, subject), |
| 59 | unique (provider, user_id) |
| 60 | ); |
| 61 | |
| 62 | ( |
| 63 | org_id bigint not null references accounts (id) on delete cascade, |
| 64 | user_id bigint not null references users (account_id) on delete cascade, |
| 65 | role text not null check (role in ('owner', 'member')), |
| 66 | created_at timestamptz not null default now, |
| 67 | primary key (org_id, user_id) |
| 68 | ); |
| 69 | on org_members (user_id); |
| 70 | |
| 71 | ( |
| 72 | id_hash text primary key, |
| 73 | user_id bigint not null references users (account_id) on delete cascade, |
| 74 | ip text, |
| 75 | user_agent text, |
| 76 | created_at timestamptz not null default now, |
| 77 | last_seen_at timestamptz not null default now, |
| 78 | expires_at timestamptz not null |
| 79 | ); |
| 80 | on sessions (user_id); |
| 81 | |
| 82 | -- Personal access tokens: the only credential git, docker and the CLI accept. |
| 83 | ( |
| 84 | id bigserial primary key, |
| 85 | user_id bigint not null references users (account_id) on delete cascade, |
| 86 | name text not null, |
| 87 | token_hash text not null unique, |
| 88 | token_prefix text not null, |
| 89 | scopes text[] not null default '{repo,packages,user}', |
| 90 | last_used_at timestamptz, |
| 91 | expires_at timestamptz, |
| 92 | created_at timestamptz not null default now |
| 93 | ); |
| 94 | on access_tokens (user_id); |
| 95 | |
| 96 | ( |
| 97 | id bigserial primary key, |
| 98 | user_id bigint not null references users (account_id) on delete cascade, |
| 99 | title text not null, |
| 100 | public_key text not null, |
| 101 | fingerprint text not null unique, |
| 102 | last_used_at timestamptz, |
| 103 | created_at timestamptz not null default now |
| 104 | ); |
| 105 | on ssh_keys (user_id); |
| 106 | |
| 107 | -- `ig login`: the CLI holds device_code, the person types user_code in a browser. |
| 108 | ( |
| 109 | device_code_hash text primary key, |
| 110 | user_code text not null unique, |
| 111 | client_name text not null default '', |
| 112 | user_id bigint references users (account_id) on delete cascade, |
| 113 | approved_at timestamptz, |
| 114 | denied_at timestamptz, |
| 115 | consumed_at timestamptz, |
| 116 | expires_at timestamptz not null, |
| 117 | created_at timestamptz not null default now |
| 118 | ); |
| 119 | |
| 120 | ( |
| 121 | id bigserial primary key, |
| 122 | owner_id bigint not null references accounts (id) on delete cascade, |
| 123 | name citext not null, |
| 124 | description text not null default '', |
| 125 | visibility text not null check (visibility in ('public', 'private')), |
| 126 | default_branch text not null default 'main', |
| 127 | size_bytes bigint not null default 0, |
| 128 | is_empty boolean not null default true, |
| 129 | archived boolean not null default false, |
| 130 | created_at timestamptz not null default now, |
| 131 | updated_at timestamptz not null default now, |
| 132 | pushed_at timestamptz, |
| 133 | unique (owner_id, name) |
| 134 | ); |
| 135 | on repos using gin (name gin_trgm_ops); |
| 136 | on repos (pushed_at desc nulls last) where visibility = 'public'; |
| 137 | |
| 138 | ( |
| 139 | repo_id bigint not null references repos (id) on delete cascade, |
| 140 | user_id bigint not null references users (account_id) on delete cascade, |
| 141 | permission text not null check (permission in ('read', 'write', 'admin')), |
| 142 | created_at timestamptz not null default now, |
| 143 | primary key (repo_id, user_id) |
| 144 | ); |
| 145 | on repo_collaborators (user_id); |
| 146 | |
| 147 | ( |
| 148 | id bigserial primary key, |
| 149 | repo_id bigint not null references repos (id) on delete cascade, |
| 150 | pusher_id bigint references users (account_id) on delete set null, |
| 151 | ref_name text not null, |
| 152 | before_sha text not null, |
| 153 | after_sha text not null, |
| 154 | commit_count integer not null, |
| 155 | head_message text not null default '', |
| 156 | via text not null check (via in ('http', 'ssh', 'web')), |
| 157 | created_at timestamptz not null default now |
| 158 | ); |
| 159 | on push_events (pusher_id, created_at desc); |
| 160 | on push_events (repo_id, created_at desc); |
| 161 | |
| 162 | -- One row per commit that landed on a repo's default branch and whose author |
| 163 | -- email is verified by a user. Idempotent, so force pushes never double count. |
| 164 | ( |
| 165 | repo_id bigint not null references repos (id) on delete cascade, |
| 166 | sha text not null, |
| 167 | user_id bigint not null references users (account_id) on delete cascade, |
| 168 | day date not null, |
| 169 | primary key (repo_id, sha) |
| 170 | ); |
| 171 | on contribution_commits (user_id, day); |
| 172 | |
| 173 | -- Git LFS. Objects are stored once in R2 by oid; repo links gate access. |
| 174 | ( |
| 175 | oid text primary key, |
| 176 | size bigint not null, |
| 177 | created_at timestamptz not null default now |
| 178 | ); |
| 179 | |
| 180 | ( |
| 181 | repo_id bigint not null references repos (id) on delete cascade, |
| 182 | oid text not null references lfs_objects (oid) on delete cascade, |
| 183 | created_at timestamptz not null default now, |
| 184 | primary key (repo_id, oid) |
| 185 | ); |
| 186 | on repo_lfs_objects (oid); |
| 187 | |
| 188 | -- Container images. Visibility is independent of any linked repo. |
| 189 | ( |
| 190 | id bigserial primary key, |
| 191 | owner_id bigint not null references accounts (id) on delete cascade, |
| 192 | -- Image path below the owner, lowercase: "api" or "tools/builder". |
| 193 | name text not null, |
| 194 | visibility text not null default 'private' check (visibility in ('public', 'private')), |
| 195 | repo_id bigint references repos (id) on delete set null, |
| 196 | description text not null default '', |
| 197 | pull_count bigint not null default 0, |
| 198 | created_at timestamptz not null default now, |
| 199 | updated_at timestamptz not null default now, |
| 200 | unique (owner_id, name) |
| 201 | ); |
| 202 | |
| 203 | -- Layers and configs, stored once in R2 under their digest. |
| 204 | ( |
| 205 | digest text primary key, |
| 206 | size bigint not null, |
| 207 | created_at timestamptz not null default now |
| 208 | ); |
| 209 | |
| 210 | -- Which packages may see a blob. Every HEAD, GET and mount checks this, so a |
| 211 | -- digest from someone's private image is useless without access to it. |
| 212 | ( |
| 213 | package_id bigint not null references packages (id) on delete cascade, |
| 214 | digest text not null references blobs (digest) on delete cascade, |
| 215 | created_at timestamptz not null default now, |
| 216 | primary key (package_id, digest) |
| 217 | ); |
| 218 | on package_blobs (digest); |
| 219 | |
| 220 | ( |
| 221 | id bigserial primary key, |
| 222 | package_id bigint not null references packages (id) on delete cascade, |
| 223 | digest text not null, |
| 224 | media_type text not null, |
| 225 | content bytea not null, |
| 226 | -- Sum of the config and layer sizes, for display. |
| 227 | total_size bigint not null default 0, |
| 228 | created_at timestamptz not null default now, |
| 229 | unique (package_id, digest) |
| 230 | ); |
| 231 | |
| 232 | ( |
| 233 | manifest_id bigint not null references manifests (id) on delete cascade, |
| 234 | digest text not null, |
| 235 | kind text not null check (kind in ('blob', 'manifest')), |
| 236 | primary key (manifest_id, digest) |
| 237 | ); |
| 238 | on manifest_references (digest); |
| 239 | |
| 240 | ( |
| 241 | package_id bigint not null references packages (id) on delete cascade, |
| 242 | name text not null, |
| 243 | manifest_id bigint not null references manifests (id) on delete cascade, |
| 244 | updated_at timestamptz not null default now, |
| 245 | primary key (package_id, name) |
| 246 | ); |
| 247 | on tags (manifest_id); |
| 248 | |
| 249 | -- In-progress pushes, staged on local disk under DATA_DIR/uploads/{id}. |
| 250 | ( |
| 251 | id uuid primary key, |
| 252 | package_id bigint not null references packages (id) on delete cascade, |
| 253 | user_id bigint references users (account_id) on delete set null, |
| 254 | size bigint not null default 0, |
| 255 | created_at timestamptz not null default now, |
| 256 | updated_at timestamptz not null default now |
| 257 | ); |
| 258 | |
| 259 | ( |
| 260 | id bigserial primary key, |
| 261 | version text not null unique, |
| 262 | target text not null default 'x86_64-unknown-linux-musl', |
| 263 | r2_key text not null, |
| 264 | sha256 text not null, |
| 265 | size bigint not null, |
| 266 | created_at timestamptz not null default now |
| 267 | ); |
| 268 | |
| 269 | ( |
| 270 | id bigserial primary key, |
| 271 | started_at timestamptz not null default now, |
| 272 | finished_at timestamptz, |
| 273 | status text not null default 'running' check (status in ('running', 'ok', 'failed')), |
| 274 | repo_count integer not null default 0, |
| 275 | bytes bigint not null default 0, |
| 276 | error text |
| 277 | ); |
| 278 | |
| 279 | ( |
| 280 | id bigserial primary key, |
| 281 | actor_id bigint references users (account_id) on delete set null, |
| 282 | action text not null, |
| 283 | target text not null default '', |
| 284 | meta jsonb not null default '{}', |
| 285 | ip text, |
| 286 | created_at timestamptz not null default now |
| 287 | ); |
| 288 | on audit_log (created_at desc); |