Scaffold PIG and model the compute-GTM ontology

PIG is an agent-native CRM for two-sided AI-compute companies: businesses
that buy GPU capacity from providers and resell it. Their business is the
spread between two pipelines, which is precisely what a generic CRM cannot
represent.

The load-bearing decision is the `allocations` table, joining a
capacity_commitment (what we bought, at a known cost) to a demand_deal
(what we sold, at a known price). Margin, utilisation and idle capacity all
fall out of that one join. Cost is charged against the full commitment
rather than only the hours that sold, because unsold hours are already paid
for and any other treatment flatters a block that is losing money.

Domain decisions worth noting, each grounded in how this market operates:

- Demand stages put `legal` second, not last. Customers do not hand
  workloads to an infrastructure provider before paper is executed.
- Supply qualification splits technical from financial diligence, recorded
  attributably. Accepting capacity is a two-key decision.
- Capacity carries a time SHAPE (intervals + quantities), not a window.
  Commitments ramp and step down; a rectangle reports availability that
  does not exist in the month someone wants it.
- SLAs model three distinct shapes: none, a reliability tier plus credits
  policy, and a negotiated agreement. Aggregators generally cannot promise
  uptime on resold capacity, but negotiate heavyweight paper upstream.
  Remedies include fee abatement, which is materially better than a capped
  credit and is not expressible as one.
- Export control is a predicate on the allocation edge, evaluated against
  the ULTIMATE parent's jurisdiction. Country of incorporation is not a
  valid key, so this cannot live as a flag on an account.
- Agent-derived claims land in `facts` with a confidence band and evidence.
  Only verified claims self-apply; weaker ones await review.
- The API never calls the agent. It writes to a leased queue, guarded by a
  partial unique index on unfinished work.

Verified: typechecks clean, migration generates and applies to Postgres 16
(31 tables, 24 enums, 117 indexes).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 18:41:41 -07:00
commit d36762f264
33 changed files with 12342 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
/**
* Identity and authorization.
*
* PIG authenticates with an external identity provider but authorizes from this
* table. The distinction is load-bearing: the auth project may be shared with
* other applications, so "has a valid token" must never imply "may use PIG".
* A token gets you as far as `GET /api/me`, which returns 404 until an
* administrator or a valid invite creates the row below.
*/
import {
boolean,
index,
jsonb,
pgTable,
primaryKey,
text,
timestamp,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import { teamEnum, teamRoleEnum } from './enums';
export const users = pgTable(
'users',
{
id: uuid('id').primaryKey().defaultRandom(),
/**
* The external identity provider's subject claim. This is the join to
* authentication and the only thing a bearer token proves.
* Nullable so that a person can be invited before they first sign in.
*/
authSubject: uuid('auth_subject'),
email: text('email').notNull(),
name: text('name').notNull(),
/** Short handle used in mentions and CLI output. */
handle: text('handle'),
avatarUrl: text('avatar_url'),
title: text('title'),
timezone: text('timezone'),
/**
* Platform administration is separate from being a member of any team, and
* separate again from being an employee of the organisation PIG tracks.
* Conflating the three is how an operator quietly ends up recorded as staff
* of a company they do not work for.
*/
isPlatformAdmin: boolean('is_platform_admin').notNull().default(false),
/** Set when the person stops using PIG. Rows are retained for audit. */
deactivatedAt: timestamp('deactivated_at', { withTimezone: true }),
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('users_auth_subject_key').on(t.authSubject),
uniqueIndex('users_email_key').on(t.email),
uniqueIndex('users_handle_key').on(t.handle),
],
);
/**
* Team membership. A person may belong to several teams — in a small company
* the same individual routinely sells capacity and sources it — so this is a
* join table rather than a column on `users`.
*/
export const teamMemberships = pgTable(
'team_memberships',
{
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
team: teamEnum('team').notNull(),
role: teamRoleEnum('role').notNull().default('member'),
/** The team shown on sign-in when someone belongs to more than one. */
isPrimary: boolean('is_primary').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
primaryKey({ columns: [t.userId, t.team] }),
index('team_memberships_team_idx').on(t.team),
],
);
/**
* Invitations. Self-serve profile creation is gated on one of these rather than
* on the identity provider's own signup setting, which may be shared with
* unrelated applications and toggled by someone with no knowledge of PIG.
*/
export const invites = pgTable(
'invites',
{
id: uuid('id').primaryKey().defaultRandom(),
/** Hashed, never stored in the clear. */
codeHash: text('code_hash').notNull(),
/** Optional pinning to one address; null allows any recipient. */
email: text('email'),
team: teamEnum('team'),
role: teamRoleEnum('role').notNull().default('member'),
createdByUserId: uuid('created_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
expiresAt: timestamp('expires_at', { withTimezone: true }),
/** Number of times this invite may still be redeemed. */
usesRemaining: jsonb('uses_remaining').$type<number>().notNull().default(1),
redeemedByUserId: uuid('redeemed_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
redeemedAt: timestamp('redeemed_at', { withTimezone: true }),
revokedAt: timestamp('revoked_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('invites_code_hash_key').on(t.codeHash),
index('invites_email_idx').on(t.email),
],
);
/**
* API keys, for the CLI and for MCP clients connecting over HTTP.
*
* An agent acting for a person is not the same principal as that person: it
* gets its own key, its own audit trail, and can be revoked without disturbing
* the human's session. Only the hash is stored, so a leaked database does not
* yield working credentials.
*/
export const apiKeys = pgTable(
'api_keys',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
keyHash: text('key_hash').notNull(),
/** Non-secret leading fragment, so a key can be identified in a list. */
keyPrefix: text('key_prefix').notNull(),
/** Coarse scopes. `read` is the default and is enough for most agent use. */
scopes: jsonb('scopes').$type<string[]>().notNull().default(['read']),
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
expiresAt: timestamp('expires_at', { withTimezone: true }),
revokedAt: timestamp('revoked_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('api_keys_key_hash_key').on(t.keyHash),
index('api_keys_user_idx').on(t.userId),
],
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type TeamMembership = typeof teamMemberships.$inferSelect;
export type ApiKey = typeof apiKeys.$inferSelect;