Skip to content

feat(auth): single-owner first-boot model#1

Merged
KnotzerIO merged 5 commits into
mainfrom
feat/owner-first-boot
Jun 26, 2026
Merged

feat(auth): single-owner first-boot model#1
KnotzerIO merged 5 commits into
mainfrom
feat/owner-first-boot

Conversation

@KnotzerIO

Copy link
Copy Markdown
Owner

Summary

Implements the single-user / "first registrant becomes owner" model. MatchaChoice is one user per instance — no roles, no multi-user.

  • Fresh instance (no user): registration is open; the first person to register (email + password) becomes the owner.
  • Once a user exists: registration is closed permanently; only login remains.
  • A second account can never be created — enforced at the data layer, not just the UI.

Changes

  • ownerExists() helper (user count > 0) in @matchachoice/auth.
  • databaseHooks.user.create.before throws APIError("FORBIDDEN", …) once an owner exists, so any endpoint hitting user-create (incl. direct signUp calls) is rejected.
  • Redirect funnel based on owner existence + session:
    • / → logged in: /dashboard; owner exists: /login; else: /signup
    • /login → logged in: /dashboard; no owner yet: /signup
    • /signup → logged in: /dashboard; owner exists: /login
  • The home page (/) was previously an ASCII landing; it is now the entry funnel.

Out of scope (not built)

Roles, multi-user, invitations, password reset, OAuth/SSO, magic links.

Verification

  • pnpm check (Ultracite) — green
  • pnpm check-types — green
  • SKIP_ENV_VALIDATION=1 pnpm build — green
  • Negative path tested against local DB: with an owner present, a direct server-side auth.api.signUpEmail call is rejected with FORBIDDEN and no second account is created.

Notes for reviewer

  • The before hook has a theoretical TOCTOU race on simultaneous first-registrations; acceptable for a single-user self-hosted instance (low traffic, one-time event).
  • Fresh-boot (empty DB → /signup → register → /dashboard) was validated by logic/build; not run live because the local dev DB already contains one user. Happy to verify on a throwaway DB if desired.

Single-user per instance: the first registrant becomes the owner and
registration is then permanently closed.

- ownerExists() helper (user count > 0) in the auth package
- databaseHooks.user.create.before rejects signups with FORBIDDEN once an
  owner exists, so no endpoint can create a second account
- redirect funnel in /, /login, /signup based on owner existence and session
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
matchachoice Ready Ready Preview, Comment Jun 26, 2026 10:07pm

In the single-owner model the redirect funnel makes these links circular
(/signup redirects to /login once an owner exists, and vice versa), so the
'Already have an account?' / 'Don't have an account?' prompts no longer apply.
Tears down the postgres volume and re-applies migrations for a clean
slate, useful for testing the first-boot owner registration flow.
@KnotzerIO
KnotzerIO merged commit 75bf9a7 into main Jun 26, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant