feat(spaces): add spaces pg schema — credentials + module_permissions

Groundwork for server-side Space extensions that must NOT live in Dexie:
  - spaces.credentials         — per-space OAuth tokens, API keys, SMTP
                                 configs. Access tokens are stored
                                 encrypted at rest with the service KEK.
  - spaces.module_permissions  — role × module read/write/admin overrides
                                 on top of the SPACE_MODULE_ALLOWLIST
                                 defaults.

Both tables FK to auth.organizations with ON DELETE CASCADE so deleting
a space drops its credentials and permission overrides automatically.

RLS is intentionally deferred — enabling it now would lock out services
that don't yet pass space context. A follow-up migration turns it on
after mana-api speaks the Spaces protocol end-to-end.

To apply locally: bun run db:push in services/mana-auth, or psql -f
sql/004_spaces.sql against the mana_platform DB.

No runtime code reads these tables yet — they're the scaffolding that
Task-8 (mana-sync) and the eventual social-relay/clubs modules will
consume.

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:13:33 +02:00
parent da1bb2d6e9
commit 698ffe797c
3 changed files with 167 additions and 0 deletions

View file

@ -0,0 +1,70 @@
-- Migration 004: Spaces schema
--
-- Adds the `spaces` schema with two server-side tables that extend Better
-- Auth organizations for our multi-tenancy model. See
-- docs/plans/spaces-foundation.md for the full RFC, and the Drizzle
-- definitions at src/db/schema/spaces.ts.
--
-- Why a separate schema:
-- - Keeps auth tables focused on identity, not domain extensions
-- - Lets us grant narrower RLS policies per schema
-- - Mirrors the pgSchema-per-concern pattern used across mana_platform
--
-- Idempotent: re-running on a partially-migrated DB is safe.
-- ─── Schema ──────────────────────────────────────────────────────
CREATE SCHEMA IF NOT EXISTS spaces;
-- ─── credentials ────────────────────────────────────────────────
-- Per-space external credentials: OAuth tokens, API keys, SMTP configs.
-- NEVER stored client-side — these are server-held secrets, wrapped with
-- the service-wide KEK (same mechanism as auth.encryption_vaults).
CREATE TABLE IF NOT EXISTS spaces.credentials (
space_id TEXT NOT NULL,
provider TEXT NOT NULL,
access_token_encrypted TEXT NOT NULL,
refresh_token_encrypted TEXT,
expires_at TIMESTAMPTZ,
scopes TEXT[],
provider_account_id TEXT,
metadata_json TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (space_id, provider),
CONSTRAINT space_credentials_space_fk
FOREIGN KEY (space_id) REFERENCES auth.organizations (id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS space_credentials_space_idx
ON spaces.credentials (space_id);
-- ─── module_permissions ─────────────────────────────────────────
-- Role × module permission matrix. If no row exists for a given
-- (space, role, module) tuple, the default is derived from SPACE_MODULE_ALLOWLIST
-- plus role-tier fallback (owner > admin > member). Rows here are
-- explicit overrides — typically written when a space owner customises
-- the default permissions for a custom role.
CREATE TABLE IF NOT EXISTS spaces.module_permissions (
space_id TEXT NOT NULL,
role TEXT NOT NULL,
module_id TEXT NOT NULL,
can_read BOOLEAN NOT NULL DEFAULT TRUE,
can_write BOOLEAN NOT NULL DEFAULT FALSE,
can_admin BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (space_id, role, module_id),
CONSTRAINT space_module_permissions_space_fk
FOREIGN KEY (space_id) REFERENCES auth.organizations (id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS space_module_permissions_space_module_idx
ON spaces.module_permissions (space_id, module_id);
-- ─── RLS ─────────────────────────────────────────────────────────
-- Defer enabling RLS until the rest of the app is scope-aware. Turning
-- it on now would lock out services that don't yet pass the space
-- context. Re-enable in a follow-up migration once mana-api speaks the
-- Spaces protocol end-to-end.
--
-- ALTER TABLE spaces.credentials ENABLE ROW LEVEL SECURITY;
-- ALTER TABLE spaces.module_permissions ENABLE ROW LEVEL SECURITY;