Skip to main content

auth-org-onboarding

Why It Exists

The gap this skill closes isn't hypothetical: a brand-new signup with zero org membership hits a 403/no-access wall on every org-gated page, and it has been independently observed and fixed in more than one fleet repo with genuinely different underlying architectures. The introducing commit (6fb18e8, feat(skills): add auth-org-onboarding skill (--org-resolution live-db|jwt-embedded)) is explicit that this is a repeated fleet problem, not a one-off bug report.

That commit also names exactly where each reference implementation came from: live-db was "ported near-verbatim from jobweave's production onboarding.ts," resolving org membership via a fresh DB lookup on every request with no session-refresh step. jwt-embedded was "adapted from work-loom," minting org id/role into the JWT via a Supabase Auth Access Token Hook instead. These aren't two implementations of the same idea — they're two different fleet repos independently arriving at different architectures because of how each one's existing auth stack resolves membership at request time.

The jwt-embedded half is where the skill's hard-won specificity comes from. Per the introducing commit, that reference "went through a real reject/fix/re-approve review cycle": a first architect pass caught a fabricated claim in an early draft — that the skill had "fixed" work-loom's org-name uniquify logic, when work-loom's version was never broken — plus three real bugs introduced while porting access-token-hook.sql from work-loom's source: dropped RLS policies, a wrong join column, and a null-unsafe claim merge. All three were re-derived from work-loom's actual source and fixed before a second review pass approved the reference.

A follow-up commit (783542e, found via git log --follow on this skill) shows the skill needed one more correction after landing: an unquoted colon inside a plain YAML scalar in SKILL.md's frontmatter description broke the pre-existing symlink-based skills add -g CLI, fixed by quoting the scalar (per docs/sessions/2026-08-14-skill-distribution-rollout/SUMMARY.md).

What It Does

auth-org-onboarding scaffolds a POST /onboarding endpoint plus its matching web-integration snippet, selected by a required --org-resolution live-db|jwt-embedded flag with no default. Both variants check for an existing membership before writing anything (idempotent-by-membership-check- first) and share a faithfully-ported slug/org-name uniquification helper (suffix loop, then a short-random backstop).

Where they diverge is what happens after the org is created. live-db resolves membership via a live DB lookup on every request, so a brand-new org is visible on the very next page load with no extra step — the dashboard's server layout just self-heals silently. jwt-embedded bakes org id/role into the JWT itself via a Supabase Auth Access Token Hook, so the client's existing token is stale until an explicit post-onboarding refresh runs. Using the wrong web-integration pattern for a repo's actual architecture leaves the new org invisible to the client indefinitely, with no error anywhere — which is why the flag has no default.

How To Use It

Triggers on: "onboarding gap", "auto-create org on signup", "no org membership after signup", "you don't have access after signup", "org-resolution live-db vs jwt-embedded", "access token hook org_id", "session refresh after onboarding", "advisory lock org creation race", or closing the org-onboarding gap as a follow-up to the auth-email-provisioning/auth-transactional-emails skills.

skills add git@github.com:catesandrew/next-starters.git --skill skills/auth-org-onboarding -g
npm install @next-starters/skill-auth-org-onboarding
/plugin marketplace add catesandrew/next-starters
/plugin install auth-org-onboarding@next-starters

Gotchas & Invariants

  • --org-resolution has no default on purpose — it's a real, observed drift risk. A silently-defaulted value would write the wrong web-integration snippet into a repo already committed to the other architecture, leaving a server-created org invisible to the client forever. Same fail-closed pattern as auth-email-provisioning's --ses-region.
  • Both variants must check for an existing membership before any write — this is what makes calling the endpoint unconditionally on every dashboard (live-db) or /welcome (jwt-embedded) load safe and cheap.
  • The pg_advisory_xact_lock race protection in the jwt-embedded reference is the ONE deliberate deviation from its work-loom source, which relies only on DB constraints and returns a raw 500 (not a clean existing response) on a same-user race — this is explicitly commented in the code, not a silent "ours is better" claim.
  • The advisory lock only serializes calls from the same user. A genuine cross-user slug/name collision under real concurrent load remains a known, open limitation in both variants — don't claim either "never 500s" on a collision.
  • The slug/org-name uniquification loop (50 numeric-suffix attempts, then a short-random backstop) is already correct in both source repos and is ported faithfully, not fixed — it's unit-tested directly via bin/__tests__/org-uniquify.test.mjs.
  • access-token-hook.sql must keep RLS SELECT policies for supabase_auth_admin (not just GRANTs) and null-safe coalesce(...) || jsonb_build_object(...) claim merging — a bare jsonb_set silently no-ops when app_metadata is absent, and dropping the RLS policies makes the hook silently read zero rows, falling through to the empty-org fallback for every login.
  • jwt-embedded needs an explicit post-onboarding refresh, and the two real mechanisms are NOT interchangeable: supabase.auth.refreshSession() for a direct Supabase Auth client, versus a session-management framework's own re-fetch (e.g. NextAuth's useSession().update()) performed inside that framework's own session callback for a framework-fronted app.
  • Porting the live-db self-heal pattern (no refresh, silent server-layout call) onto a jwt-embedded repo is the single most likely cross-variant mistake — that dashboard's authorization check reads a stale JWT claim, not a live DB lookup.
  • SKILL.md's YAML frontmatter description must stay a quoted scalar — an earlier unquoted version broke the symlink-based skills add -g CLI on an embedded colon (fixed in 783542e).
  • auth-email-provisioning — provisions the SES/Cloudflare/Turnstile infra for the signup flow this onboarding gap sits immediately after, with no org-provisioning concerns of its own.
  • auth-transactional-emails — authors the branded confirmation/recovery templates sent during that same signup flow, also with no org-provisioning concerns of its own.

Sourced from: skills/auth-org-onboarding/metadata.json, skills/auth-org-onboarding/SKILL.md, docs/sessions/2026-08-14-skill-distribution-rollout/SUMMARY.md, git log --follow -- skills/auth-org-onboarding (commits 6fb18e8, 783542e)