Joiner / Mover / Leaver Procedure
Status: written ahead of the event it describes
Stratify is a single-engineer team today (register/facts.md, "Team / access" — one human identity across all commits in git log). Nobody has ever joined, moved, or left under a formal process, because nobody besides Toby has held technical access to begin with. This procedure is written for the first hire or contractor, not as a retrofit of something that has already happened. Treat every step below as the target process, not a description of past practice.
The two ISMS roles that exist today — ISMS Owner (Toby) and DPO/Privacy Owner (Antonios) — were assigned by direct confirmation with Toby on 2026-08-01 (register/facts.md), not through this procedure. This procedure governs everyone who comes after.
Scope
Applies to anyone who is granted, changes, or loses access to Stratify systems: engineers, contractors, and any future non-engineering hire who needs elevated application roles (e.g. a compliance-role account). It does not cover subscriber or partner account lifecycle — see PRO-03 for partner onboarding/offboarding and PRIV-09 for subscriber data-subject rights.
Systems in scope for any access grant or revocation:
- GitHub (
thoughtful-toby/stratifyrepo — read, write, or admin) - Supabase / Postgres: application-level
subscriber_roles(admin,founder,compliance
roles per apps/web/lib/auth/roles.ts:4), and the Supabase service-role API key, which structurally bypasses RLS (register/facts.md, "Database & access control")
- Hetzner production host: Tailscale network membership + SSH access
(docs/runbooks/deploy.md:3-19)
- Secrets: macOS Keychain entries and any production env vars the role needs
(docs/secrets.md)
- Any third-party vendor console the role needs (Sentry, PostHog, AWS SES, Stripe)
1. Joiner
Least-privilege by default: grant only the access a role needs to do its actual work, not a copy of Toby's own access.
- Role definition. Before access is granted, write down in one sentence what the person
does and which application role(s) they need (subscriber, pilot, compliance, founder, admin — apps/web/lib/auth/roles.ts:4), whether they need repository write access, and whether they need any production system access at all. Most contractor roles (e.g. a frontend contractor) need none of the latter two.
- GitHub access. Add as a collaborator with the minimum role (read for review-only,
write for a contributor who merges their own PRs under the CI gate). Do not grant admin unless the person is taking over ISMS Owner or repository-administration responsibilities.
- Application role grant. If the role needs an elevated
subscriber_rolesentry
(admin, founder, compliance), the ISMS Owner inserts the row via the admin action path or a direct, audited service-role write — never by sharing the service-role key itself with the new person. Elevated writes already call recordAudit() (register/facts.md), so the grant itself becomes an audit_log entry.
- Secrets / Keychain access, if applicable. Only for engineering roles that need local
development access. Follow docs/secrets.md's existing pattern: the person adds their own Keychain entries for secrets they're handed, sourced via their own ~/.keychain-secrets.sh. No secret is ever transmitted in plaintext over chat or email — hand it over via a channel that isn't persisted (verbal, or a password manager share), then have the person confirm the Keychain entry exists.
- Production/Tailscale access, if applicable. Only for a second engineer who needs to
operate the Hetzner host directly. Add them to the Tailscale network as a separate identity (not a shared key), and confirm ufw's Tailscale-only policy still applies (docs/runbooks/deploy.md:3-19).
- Security-awareness briefing. Before the person's first production-affecting change, walk
through the onboarding checklist in GOV-09: CI gate and secure-coding basics, secrets handling, the incident-reporting path (PRO-08 once written), and access-provisioning/removal expectations (this document). Record a dated, named acknowledgement that the briefing happened — this is the evidence record for this step.
- Evidence produced. A dated access-change log entry (see "Evidence" below) plus the
audit_log row(s) from step 3, plus the signed GOV-09 acknowledgement from step 6.
2. Mover
A role change (e.g. a contractor becomes a full engineer, or someone moves off the compliance role) is treated as a joiner-for-the-new-access and a leaver-for-the-old-access, not a silent edit.
- Determine the new role definition, same one-sentence exercise as Joiner step 1.
- Provision the new access following the relevant Joiner steps above for whatever is newly
needed.
- Revoke the access that no longer matches the new role — do not leave old
subscriber_roles
entries, GitHub permissions, or Keychain-shared secrets in place "just in case." If the person is dropping the compliance role, remove that subscriber_roles row explicitly; don't rely on it becoming stale.
- Evidence produced. A dated access-change log entry recording both the grant and the
revocation, with the audit_log row IDs for each.
3. Leaver
Leaver revocation is the highest-priority case here — an unrevoked credential after departure is a live risk, not a documentation gap. docs/secrets.md's rotation table already states the governing rule: "Engineer leaves → rotate every secret they could have read. Service-role first." This procedure operationalizes that line into a checklist.
Leaver checklist (ISMS Owner executes, or delegates and verifies):
- GitHub — remove the person as a collaborator; if they held
admin, transfer or revoke
any personal access tokens tied to their account that touch the repo.
- Supabase / application roles — remove their
subscriber_rolesrow(s) entirely (don't
just downgrade). If they ever held or could have read the service-role key, treat the service-role key itself as compromised — see step 4.
- Supabase / DB access — if they had direct Postgres access (e.g. via
scripts/with-stratify-hetzner-supabase-db.sh or a personal STRATIFY_DATABASE_URL), revoke that connection string / credential.
- Secrets rotation — rotate every secret the person could have read, service-role key
first, per docs/secrets.md's rotation table. In practice today that means, at minimum: STRATIFY_SUPABASE_SERVICE_ROLE, STRATIFY_DATABASE_URL, and any vendor key (STRATIFY_STRIPE_SECRET_KEY, AWS SES keys, STRATIFY_VAPID_PRIVATE_KEY, STRATIFY_CRON_SECRET, STRATIFY_PARTNER_WEBHOOK_SECRET) they had Keychain access to or could reach on the production host. Use the manual deploy path (infra/hetzner/deploy-web-hetzner.sh) to push rotated production secrets — the pull-based deploy timer never writes secrets (docs/runbooks/deploy.md, "Deliberately not automated").
- Tailscale / host access — remove their Tailscale node/identity from the tailnet; confirm
ssh [email protected] no longer accepts their key.
- Vendor consoles — remove their access from Sentry, PostHog, AWS SES, Stripe, and any
other third-party console they had a seat on.
- Local Keychain entries — this is the departing person's own responsibility to destroy on
their own machine, but the ISMS Owner should not treat that as sufficient — rotation (step 4) is what actually neutralizes the risk, regardless of what happens to their local copy.
- Confirm and log. Once all of the above is done, write the dated access-change log entry
listing exactly what was revoked/rotated and when. This is the evidence artifact for the leaver event.
Evidence
Every joiner/mover/leaver event produces a dated markdown entry under docs/compliance/isms/evidence/, one file per event (e.g. evidence/2026-MM-DD-leaver-<name-or-role>.md), listing:
- Event type (joiner/mover/leaver), person, role, date
- Each access system touched and the action taken (granted/changed/revoked)
- Relevant
audit_logrow ID(s), where the action went through an audited code path - Who executed the checklist and who verified it (today, the same person — see PRO-02 for why
that's an honestly-stated limitation, not a hidden one)
Review
This procedure has never been exercised. It should be reviewed and, if necessary, corrected the first time it actually runs — a written-ahead-of-practice procedure that survives its first real use unchanged is the exception, not the rule. Otherwise reviewed annually alongside the other Tier 2 procedures.