feat(spaces): auto-create personal space on signup via Better Auth hook

Wires databaseHooks.user.create.after to call createPersonalSpaceFor,
which provisions a Better Auth organization of type='personal' with the
user as owner. Every signup now produces a usable default space — no
UI code needed to bootstrap it.

Details:
- Slug derived from email local-part, lowercase, alphanumerics + hyphens,
  max 30 chars, random fallback if nothing usable remains.
- Reserved-slug list (me/admin/api/auth/…) blocks system-route clashes.
- Collision resolver appends -2, -3, … up to 999 before falling back to
  a random suffix. Tests cover both the DB-taken and reserved-slug cases
  via an injectable SlugTakenLookup (no DB needed for unit tests).
- Idempotent: if a personal space already exists for the user, returns
  it instead of creating a duplicate. Guards against retry double-signup.
- Failure propagates — an orphan user without a personal space is worse
  than a retry-able signup error.

Existing dev users will need a backfill or a re-provisioning of the dev
DB — new users are unaffected.

12 tests pass (23 total across the spaces module).

Plan: docs/plans/spaces-foundation.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-20 16:10:26 +02:00
parent 5af96bfeff
commit da1bb2d6e9
4 changed files with 266 additions and 1 deletions

View file

@ -38,7 +38,11 @@ import {
} from '../email/send';
import { sourceAppStore, passwordResetRedirectStore } from './stores';
import { TRUSTED_ORIGINS } from './sso-origins';
import { assertValidSpaceMetadataForCreate, assertSpaceIsDeletable } from '../spaces';
import {
assertValidSpaceMetadataForCreate,
assertSpaceIsDeletable,
createPersonalSpaceFor,
} from '../spaces';
// Re-export so existing imports (`import { TRUSTED_ORIGINS } from './better-auth.config'`)
// keep working. New code should import from './sso-origins' directly.
@ -202,6 +206,32 @@ export function createBetterAuth(databaseUrl: string) {
updateAge: 60 * 60 * 24, // Update session once per day
},
/**
* Database hooks lifecycle callbacks for core tables.
*
* `user.create.after` runs after a successful signup and provisions
* the user's personal Space (a Better Auth organization of type
* `personal`). Every user needs one because modules store private
* data like mood, dreams, sleep there. Failure propagates: an
* orphan user without a personal space is a worse state than a
* retry-able signup error.
*
* See docs/plans/spaces-foundation.md and ../spaces/personal-space.ts.
*/
databaseHooks: {
user: {
create: {
after: async (user) => {
await createPersonalSpaceFor(db, {
id: user.id,
email: user.email,
name: user.name,
});
},
},
},
},
// Base URL for callbacks and redirects
baseURL: process.env.BASE_URL || 'http://localhost:3001',