package calls import "time" // Models is every model the schema generator manages. Keep it in step // with the structs below: `rastrillo migration generate` reads it, and // `rastrillo migration check` fails CI when it and the migrations // disagree. var Models = []any{ &Person{}, &Meeting{}, &MeetingLink{}, &GuestGrant{}, &PendingJoin{}, &Agent{}, &MeetingDefaults{}, &DeviceToken{}, } // Person is a verified identity or an anonymous guest. Anonymous guests carry // an opaque guest: identifier in Email, never a claimed or verified address. // Standing (member, guest, none) is computed per request, never stored. type Person struct { Anonymous bool `gorm:"not null;default:false"` ID int64 Email string `gorm:"uniqueIndex;not null"` Name string `gorm:"not null"` CreatedAt time.Time } // TableName overrides GORM's default pluralization. GORM treats // "person" as an irregular noun (via jinzhu/inflection) and would // otherwise name this table "people"; the spec and this app's schema // test both name it "persons". func (Person) TableName() string { return "persons" } // Meeting is a room that may be entered. Its title is sealed; its // times are plaintext because the list sorts on them. // // AutoTitle says the title is one Calls wrote ("Call, 2 Oct 15:40", // callTitle) rather than one a person chose: the apps show a chosen // title over the call and leave a written one off (2026-10-02). It is a // flag set where the title is made, not something read back out of the // title's words. TitleZone is the IANA zone an automatic title's time // is in — the starter's when their browser or app said, otherwise the // instance's — kept as data so the title itself can read cleanly // without naming it. type Meeting struct { RequireSignin bool `gorm:"not null;default:false"` WaitingRoom bool `gorm:"not null;default:false"` AutoTitle bool `gorm:"not null;default:false"` TitleZone string `gorm:"not null;default:''"` ID int64 Sqid string `gorm:"uniqueIndex;not null"` TitleSealed []byte `gorm:"column:title_sealed"` StartsAt *time.Time CreatedBy string `gorm:"not null"` CreatedAt time.Time EndedAt *time.Time EndedBy string } // MeetingLink looks up invitations by hash. New tokens are also sealed so // members can share the current invitation again; older rows remain hash-only. type MeetingLink struct { ID int64 MeetingID int64 `gorm:"index;not null"` TokenHash string `gorm:"uniqueIndex;not null"` TokenSealed []byte CreatedBy string `gorm:"not null"` CreatedAt time.Time RevokedAt *time.Time } // DeviceToken is one Apple device a person can be reached on when the // app is not running. It is the only part of the knock notification that // has to be stored: the queue itself lives in the hub's memory and dies // with the process, which is exactly why a person outside the room could // never be told about it. // // The token is stored in plaintext, unlike a meeting title. It is not a // secret about a person — it is an address Apple issues, scoped to one // app on one device, useless without the push key, and rotated by the // system whenever it likes. Sealing it would cost the sweep the ability // to delete by token when Apple reports one unregistered, which is the // one hygiene operation this table needs. // // Unique on the token rather than on (person, token): Apple reissues a // token to whoever installs the app next, so the same string arriving // for a different person means the device changed hands and the old row // is wrong, not that there are two. type DeviceToken struct { ID int64 PersonID int64 `gorm:"index;not null"` Token string `gorm:"uniqueIndex;not null"` // "ios" or "macos". Apple keeps one token space per app per device, // but the two apps have different bundle identifiers and therefore // different APNs topics, so a token is useless without knowing which. Platform string `gorm:"not null"` CreatedAt time.Time LastSeen time.Time } // GuestGrant is a non-member's entry authority for one meeting. One // row per person per meeting. AdmittedAt and AdmittedBy are an audit of // the first admission (slice 2) and confer nothing. type GuestGrant struct { ID int64 MeetingID int64 `gorm:"uniqueIndex:idx_grant_meeting_person;not null"` PersonID int64 `gorm:"uniqueIndex:idx_grant_meeting_person;not null"` GrantedAt time.Time ExpiresAt time.Time `gorm:"index"` AdmittedAt *time.Time AdmittedBy string } // PendingJoin is written before a magic link is mailed to someone who // followed a live meeting link, so the emailed link can admit them in // any browser and /continue can find their destination. The address is // a keyed fingerprint, never plaintext. type PendingJoin struct { ID int64 MeetingID int64 `gorm:"not null"` LinkID int64 `gorm:"not null"` AddressFingerprint string `gorm:"index;not null"` CreatedAt time.Time ExpiresAt time.Time } // Agent is a drive-surface principal (spec §8.1): a bearer token minted // by a member (the operator) that reaches exactly as far as that // member's own current standing, and no further once they leave. The // token itself is never stored — only the SHA-256 of it. type Agent struct { ID int64 Name string `gorm:"not null"` OperatorPersonID int64 `gorm:"index;not null"` TokenHash string `gorm:"uniqueIndex;not null"` CreatedAt time.Time RevokedAt *time.Time } // MeetingDefaults holds the team’s defaults for newly created meetings. type MeetingDefaults struct { ID int64 RequireSignin bool `gorm:"not null;default:false"` WaitingRoom bool `gorm:"not null;default:false"` }