← Corpus / dididecks-ai / model
Auth surface — Identity, Session, Token, Organization, Membership, AuthEvent
The auth tables that gate every deck. Already a SQLite-shaped DB (astro:db on libSQL local, Turso remote) — so unlike the .md-on-disk models, this one is closer to translation-ready. Chroma is the most recent and authoritative schema. Calmstorm is an earlier-shape version. Humain doesn't have auth installed yet.
- Path
- models/Auth-Surface-Data-Model.md
Auth Surface · Data Model
What this model represents
Everything that gates a request to a private deck:
- Who you are (Identity, OAuthAccount)
- What session you’re in (Session, MintedToken)
- What organization you belong to (Organization, Membership, FirmProfile)
- Every gating decision the system has made on your behalf (AuthEvent — append-only audit log)
Unlike the .md-on-disk models, this is already a SQL-shaped database (astro:db on libSQL locally — auth.db — and Turso remotely). So “translation to a remote DB” for this model is much smaller — it’s mostly already a remote DB. The interesting question is whether to:
- Keep the per-client auth dbs (chroma has its own libSQL/Turso; humain will get its own; calmstorm has its own) — minimal change
- Consolidate into one shared auth DB with
app_slugdiscriminator — more change, but one Identity row can attend multiple decks
Chroma’s schema already has the app_slug column on AuthEvent that supports option 2 — it’s an intentional preparation.
Where it lives
| Client | Schema file | Local DB | Remote DB |
|---|---|---|---|
calmstorm-decks | db/config.ts (older shape) | auth.db | Turso (env vars in deploy) |
chroma-decks | db/config.ts (current shape — authoritative) | auth.db | Turso |
humain-vc-decks | absent — auth not installed | absent | absent |
The canonical schema (chroma — current)
Chroma’s db/config.ts defines these tables. All id columns are text (UUIDv7 strings); FKs are text references.
Identity
The canonical person record. One per email; lossless_id is a stable cross-system UUIDv7.
columns: {
id: text primaryKey // UUIDv7
lossless_id: text unique // cross-system UUIDv7 (also used by other Lossless apps)
primary_email: text unique
label: text optional // display name override
full_name: text optional
linkedin_url: text optional
handle: text optional
avatar_url: text optional
notes: text optional
created_at: datetime default now()
first_seen_at: datetime optional
last_seen_at: datetime optional
}
OAuthAccount
GitHub (and future OAuth provider) identity linkage. Chroma supports OAuth via arctic.
columns: {
id: text primaryKey
identity_id: text references Identity.id
provider: text // "github" | (future) "google", "linkedin"
provider_subject: text // the provider's user ID
provider_email: text optional
provider_username: text optional
linked_at: datetime default now()
last_used_at: datetime optional
}
indexes: {
on [provider, provider_subject]: unique
on [identity_id]
}
MintedToken
The magic-link token system. Magic links are generated by the pnpm invite CLI; redeemed at /access/.
columns: {
id: text primaryKey // the token itself (or hash of it)
identity_id: text optional references Identity.id // null for "any-one-time-use" tokens
role: text // viewer | admin | … (gates role on session creation)
expires_at: datetime optional
max_uses: int default 1
uses_remaining: int default 1
minted_at: datetime default now()
minted_by: text optional // who minted (an Identity.id of an admin)
revoked_at: datetime optional
notes: text optional
}
Session
A live browser session. Created on magic-link redeem or passcode tier-3 fallback.
columns: {
id: text primaryKey // the session cookie value (HMAC'd)
identity_id: text optional references Identity.id // null for passcode-tier sessions (no identity attached)
tier: text // "magic-link" | "oauth" | "passcode" | …
role: text // viewer | admin | …
shared_label: text optional // a human-readable label for passcode-tier shared sessions
enrolled_at: datetime default now()
last_seen_at: datetime optional
revoked_at: datetime optional
ua_hash: text optional // hashed user-agent for tying session to browser
ip_hash: text optional // hashed IP for tying session to network
token_id: text optional references MintedToken.id // which token enrolled this session
}
Organization
Per the ai-labs CLAUDE.md “domain-as-id” convention: id is the canonical email domain.
columns: {
id: text primaryKey // "lossless.group" | "trychroma.com" | "humain.vc" | "personal"
slug: text unique // "lossless" | "chroma" | "humain" | "personal"
name: text // display name
created_at: datetime default now()
}
Every deck seeds (at minimum) two organizations on first boot: lossless.group (operating team) + {client-domain} (the client this deck represents). Personal-email signups bucket as id = "personal".
FirmProfile
VC-firm-specific metadata for an Organization that IS a VC firm.
columns: {
id: text primaryKey references Organization.id
firm_kind: text optional // "vc" | "operating-company" | "service-provider"
portfolio_path: text optional // path in repo where this firm's portfolio lives (e.g. "data/investors/bloomberg-beta/portfolio")
aum_tier: text optional // "small" | "mid" | "large"
brand_assets_path: text optional
}
Membership
Identity-Organization join table. Roles are scoped per-org.
columns: {
id: text primaryKey
identity_id: text references Identity.id
organization_id: text references Organization.id
role: text // "admin" | "viewer" | "owner" | …
joined_at: datetime default now()
revoked_at: datetime optional
}
indexes: {
on [identity_id, organization_id]: unique
on [organization_id]
}
AuthEvent — append-only audit log
Every gating decision. Never updated, only appended.
columns: {
id: text primaryKey
at: datetime default now()
app_slug: text default "chroma-decks" // ← supports cross-app consolidation
outcome: text // "allow" | "deny" | "challenge" | "error"
reason: text optional // "valid-session" | "expired-token" | "no-identity" | "wrong-passcode" | …
tier: text optional // session tier
role: text optional // session role
identity_id: text optional
organization_id: text optional
session_id: text optional
token_id: text optional
shared_label: text optional
ip_hash: text optional
ua_hash: text optional
passcode_hash_prefix: text optional // first N chars of hashed passcode attempt (for rate-limiting; never stores plaintext)
}
indexes: {
on [at]
on [identity_id]
on [app_slug]
}
The calmstorm shape (older — what NOT to follow on new clients)
Calmstorm’s db/config.ts came first and has these differences:
- No
OAuthAccounttable — calmstorm is magic-link + passcode only - No
Organization/FirmProfile/Membershiptables — no multi-org support; one client one tenant - Simpler
Identity— hasemailinstead ofprimary_email; hasorgas a text field instead of FK to Organization AuthEvent.app_slugabsent — calmstorm’s audit log doesn’t carry the app discriminator (it’s hardcoded to “calmstorm-decks” in code)
If consolidating into one shared auth DB, calmstorm rows would need to be migrated up: synthesize an Organization row from each calmstorm Identity’s org text field, then synthesize the Membership; backfill app_slug = "calmstorm-decks" on every AuthEvent.
Per-client variations
| Aspect | Calmstorm | Chroma | Humain |
|---|---|---|---|
| Schema version | v1 (no orgs) | v2 (orgs + OAuth) | not installed |
| Identity primary key column | email | primary_email | n/a |
| OAuth provider support | none | GitHub via arctic | n/a |
| Multi-org / Membership | no | yes | n/a |
lossless_id cross-system | no | yes | n/a |
AuthEvent.app_slug | implicit | explicit column | n/a |
app_slug default value | n/a | "chroma-decks" | will be "humain-vc-decks" |
When humain’s auth install plan executes (per client-sites/humain-vc-decks/context-v/plans/Install-Auth-Surface-from-Calmstorm-Pattern.md), it will use chroma’s schema verbatim with app_slug defaulted to "humain-vc-decks".
What’s load-bearing
Identity.lossless_id— the only cross-system stable ID. If consolidating to one DB, this is the FK target.Organization.id = domain— the load-bearing convention (per ai-labs CLAUDE.md).lossless.group,trychroma.com,humain.vc,calmstormvc.com. Personal-email signups bucket as"personal".AuthEvent.app_slug— required for multi-app consolidation. New AuthEvent inserts MUST carry this column.Session.identity_idoptional — passcode-tier sessions don’t have an identity. The DB cannot enforce non-null here.MintedToken.idIS the token — or the hash of it. Compromised DB = compromised tokens. Storage at rest needs to match the security tier.
Translation to a remote DB
Option A — keep per-client Turso databases (minimal change)
This is the current state. Each deployed client has its own Turso database. No schema changes; just verify each client’s db/config.ts is current with the chroma schema as it evolves.
Pros: zero-change. Per-deck isolation. Compromise of one client doesn’t expose others. Cons: no cross-deck identity. Reader has to re-authenticate per deck. No central admin surface.
Option B — consolidate to one Postgres / one Turso with app_slug discrimination
Migrate all clients’ auth tables to one shared instance. Every row carries app_slug (chroma already does on AuthEvent; add to other tables for consistency).
// Same tables as chroma's astro:db schema, with these additions:
model Identity {
// … all chroma fields …
// No app_slug — identities cross apps (the whole point).
}
model Session {
// … all chroma fields …
app_slug String // NEW: "chroma-decks" | "humain-vc-decks" | "calmstorm-decks"
@@index([app_slug, identity_id])
}
model MintedToken {
// … all chroma fields …
app_slug String // NEW
@@index([app_slug])
}
model Membership {
// … all chroma fields …
// App-scoping is via Organization (the client's domain Organization),
// not via an app_slug column. Membership semantics: this identity is a
// member of this org, which the app surface knows is "their" org.
}
model AuthEvent {
// … all chroma fields including app_slug …
}
Pros: one Linda Avey, one Aneil Mallavarapu. SSO across decks. Central admin. Cons: larger blast radius on compromise. Migration cost (especially calmstorm’s older shape).
Recommendation
Start with option A (per-client Turso). Defer option B until there’s a concrete need (e.g., a reviewer needs to authenticate across humain + chroma in one session). The schemas are mostly compatible — migration is feasible later.
Open questions for the collaborator
-
Are you trying to design ONE auth DB or per-deck DBs? The chroma schema is ready for either. Per-deck is simpler today; one shared is the destination if cross-deck SSO becomes a requirement.
-
Postgres or libSQL/Turso? Chroma + calmstorm use libSQL (because astro:db speaks libSQL natively). If switching to Postgres, the schema translates directly (
text→textorvarchar,datetime→timestamptz). Recommendation: stay on libSQL/Turso for new clients; Turso scales fine to the deck-OS use case and astro:db’s developer ergonomics matter. -
Where do new Identity rows come from? Today: magic-link redeem creates an Identity. Future: OAuth callback. Possible: passcode-tier sessions never create an Identity (anonymous reviewer). Recommendation: keep the existing flow.
-
Organization.id = domainconvention — strict or relaxed? It’s strict in chroma (every Identity gets an Organization row keyed by their email domain). If consolidating, do we honor it across apps? Recommendation: yes — it’s the load-bearing convention; cross-app identity makes more sense whenIdentity.primary_emailandMembership.organization_idalready agree on domain bucketing. -
PII at rest:
Identity.primary_email,Identity.linkedin_url,Identity.avatar_url,Session.ip_hash,Session.ua_hash— what’s the storage discipline? Chroma stores hashes for IP/UA; emails in plaintext (justifiable for invite/audit but worth surfacing for compliance review). Recommendation: revisit when the first big-firm LP onboards.
See also
client-sites/chroma-decks/db/config.ts— current authoritative schema (read this first)client-sites/calmstorm-decks/db/config.ts— older shape (read for migration context)client-sites/humain-vc-decks/context-v/plans/Install-Auth-Surface-from-Calmstorm-Pattern.md— the porting plan../../context-v/explorations/Shared-Auth-for-Applied-AI-Labs.md(inai-labs/) — parent-level exploration of the cross-app identity story- ai-labs
CLAUDE.md§ “Organization naming convention — domain-as-id”