Invite-only. No passwords.
How 29 Court onboards clients. Operators create organizations and send invites. Clients sign in with a magic link — no passwords, no friction. Every access event is logged. This is the invite-only pattern: tight control, smooth experience.
The permissions are the app. This is the whole data model — schema.ts, the first of the app's three files (its import line, not shown, brings these names in from "create-dillo/schema"):
// The schema the live backend serves today (span2/live-schemas filing, re-verified at deploy).
export default schema({
AccessEvent: table({
action: string().min(1).max(80),
actorUserId: relation.user().optional().nullable(),
detail: string().max(4000).optional().nullable(),
orgId: relation.team(),
requestId: string().max(200).optional().nullable(),
subject: string().min(1).max(200),
}).permissions({ view: members, edit: nobody }),
ClientProfile: table({
company: string().max(160).optional().nullable(),
goalsFirstWeek: string().max(2000).optional().nullable(),
goalsNeverWithoutMe: string().max(2000).optional().nullable(),
goalsOffPlate: string().max(2000).optional().nullable(),
orgId: relation.team(),
preferredName: string().min(1).max(120),
role: string().max(160).optional().nullable(),
timezone: string().max(64).optional().nullable(),
userId: relation.user(),
writingStyle: string().max(64).optional().nullable(),
}).permissions({ view: members, edit: recordOwner.or(roles('admin', 'owner')) }),
GuardrailPref: table({
actionClass: string().min(1).max(64),
level: integer().min(0).max(3),
orgId: relation.team(),
userId: relation.user(),
version: integer().min(1).max(10000),
}).permissions({ view: members, edit: recordOwner.or(roles('admin', 'owner')) }),
Invite: table({
acceptedAt: string().datetime().optional().nullable(),
company: string().max(160).optional().nullable(),
dataKind: string().max(32).optional().nullable(),
email: string().email(),
expiresAt: string().datetime(),
invitedName: string().max(120).optional().nullable(),
orgId: relation.team(),
revokedAt: string().datetime().optional().nullable(),
state: string().min(1).max(32),
token: string().max(200).optional().nullable(),
tokenUsed: boolean().optional().nullable(),
}).permissions({ view: members, edit: roles('admin', 'owner') }),
Organization: table({
dataKind: string().max(32).optional().nullable(),
name: string().min(1).max(160),
orgId: relation.team(),
status: string().min(1).max(32),
}).permissions({ view: members, edit: roles('admin', 'owner') }),
SetupSession: table({
assistantChoice: string().max(64).optional().nullable(),
orgId: relation.team(),
promptAccepted: string().max(16).optional().nullable(),
status: string().min(1).max(32),
step: string().min(1).max(48),
userId: relation.user(),
}).permissions({ view: members, edit: recordOwner.or(roles('admin', 'owner')) }),
});