Migration from user-level tier to Space-level tier, following the
Spaces foundation plan. User-visible effect: the tier that gates
module access now belongs to the active Space, not the user account.
Personal Spaces inherit the user's old tier on signup so nothing
downgrades.
shared-types:
- New SpaceTier type ('guest' | 'public' | 'beta' | 'alpha' | 'founder').
- New spaceTierMeets(actual, required) helper.
- SpaceMetadata gains an optional `tier` field.
mana-auth:
- createPersonalSpaceFor reads user.accessTier and stamps it into the
personal Space's metadata.tier. A founder-tier user setting up their
first Space keeps founder access in that Space.
- databaseHooks.user.create.after now forwards accessTier into the
personal-space creator.
apps/web (scope layer):
- ActiveSpace gains a required `tier: SpaceTier`; rawToActiveSpace
reads it from organization.metadata, defaulting to 'public' if
missing or invalid.
- New getEffectiveTier(userFallback) helper resolves the tier to use
for gating: prefers the active Space's tier, falls back to the
caller-supplied user tier during the boot window.
apps/web ((app) layout):
- `effectiveTier` $derived replaces every authStore.user?.tier reference
in the layout's access-gating logic (appItems, routeBlocked,
routeTierLabels). AuthGate deeper in the UI keeps using user.tier as
its own fallback — the tier move is additive, not destructive.
What this does NOT do yet:
- The user.accessTier column still exists and is still the initial
source for personal-space tier. Removing it is a later cleanup once
every code path reads through the Space primitive.
- No admin API for setting tier on a Space (PUT /api/v1/admin/spaces/
:id/tier). Follow-up when admin tooling needs it — today admins still
set user.accessTier, which flows to the personal space on next
signup.
Resolves the MANA_APPS-tier-patch workaround memory: future sessions
can adjust tier per Space instead of per User.
0 errors across 7151 files. 10/10 scope tests pass.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Moves the canonical SpaceType + SPACE_MODULE_ALLOWLIST to @mana/shared-types
(framework-free) so the Bun services can consume them without pulling in
Svelte. shared-branding keeps only the UI-facing labels and descriptions
and re-exports the canonical types for frontend convenience.
Wires two Better Auth organization hooks in mana-auth:
- beforeCreateOrganization asserts metadata.type is a valid SpaceType,
rejecting the create with a BAD_REQUEST otherwise.
- beforeDeleteOrganization rejects deletion of the personal space.
Covered by bun tests (11 assertions) for the helper module.
No migration and no schema change — type lives in the existing
organization.metadata jsonb column.
Plan: docs/plans/spaces-foundation.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>