npx skills add ...
npx skills add thinkoodle/rails-skills --skill identity-membership
Expert guidance for the Identity vs User split and tenant memberships in Rails. Use when adding users, workspaces, accounts, memberships, roles, join codes, invites, Current.identity, Current.user, or connecting magic-link auth to a tenant. Complements magic-link-auth and activerecord-tenanted. Not Nebula workspace members.
npx skills add thinkoodle/rails-skills --skill identity-membership
A person who signs in is an Identity. A workspace they can enter is a Tenant (or Account). The join is Membership. Sessions hang on Identity, never on the tenant-scoped User.
This sits on top of magic-link-auth. In SQLite-per-tenant apps it also sits on activerecord-tenanted. Fizzy uses the same split in a single database.
with_tenant. Order matters; a User without a Membership is an orphan.Herald skips the tenanted User: AccountMembership on the global DB is enough because account members can see every project. Cortex/Innkeeper need a User inside the tenant DB for project assignments, comments, and audit actor.
Fizzy's User is the membership row in the same database (users.identity_id + account_id). Same idea, no shard.
Sign-in creates a Session for this Identity (magic-link-auth). It does not pick a tenant.
Roles live here for "can they enter this workspace at all." Finer ACL (board access, project assignments) lives on tenanted records.
Herald: AccountMembership with owner / member. Innkeeper adds viewer.
No FK to identities — different database. Copy role from Membership at join time; don't treat the two roles as independent sources of truth.
Rescue RecordNotUnique and re-find — two tabs will race. Herald's Account#add_member! is the same idea without a tenanted User.
After magic-link verify, if identity.tenants.none?, send them to create-tenant / new-project. If they have tenants, send them to the last one or a picker.
Current.user is nil when the Identity is signed in but has no Membership in this tenant. That's a 403, not a sign-in loop.
Don't set Current.user from the session. The session doesn't know which tenant the URL selected.
Shareable XXXX-XXXX-XXXX (Base58, no 0/O/I/l). Global table. One active code per tenant/project.
Redeem:
identity.join(tenant) then redeem_if.Don't use a magic-link code as an invite. Don't increment usage until join succeeds.
has_many :sessions on User — logout/login is per person, not per workspace.find_or_create_by(email:) inside a tenant — creates duplicate people; Identity is the uniqueness boundary.Current.identity only — they might not be a member of this tenant.Member on workspace — different tenancy; don't copy that into an activerecord-tenanted app.magic-link-authactiverecord-tenantedSee reference.md for Herald vs Cortex/Innkeeper vs Fizzy, and test helpers.