-- CreateEnum CREATE TYPE "user_status" AS ENUM ('ACTIVE', 'DISABLED'); -- CreateTable CREATE TABLE "users" ( "id" UUID NOT NULL, "email" TEXT NOT NULL, "password_hash" TEXT NOT NULL, "name" TEXT NOT NULL, "status" "user_status" NOT NULL DEFAULT 'ACTIVE', "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMP(3) NOT NULL, "deleted_at" TIMESTAMP(3), CONSTRAINT "users_pkey" PRIMARY KEY ("id") ); -- CreateTable CREATE TABLE "tenant_memberships" ( "id" UUID NOT NULL, "tenant_id" UUID NOT NULL, "user_id" UUID NOT NULL, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "tenant_memberships_pkey" PRIMARY KEY ("id") ); -- CreateIndex CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); -- CreateIndex CREATE INDEX "tenant_memberships_tenant_id_idx" ON "tenant_memberships"("tenant_id"); -- CreateIndex CREATE UNIQUE INDEX "tenant_memberships_tenant_id_user_id_key" ON "tenant_memberships"("tenant_id", "user_id"); -- AddForeignKey ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_tenant_id_fkey" FOREIGN KEY ("tenant_id") REFERENCES "tenants"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "tenant_memberships" ADD CONSTRAINT "tenant_memberships_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- Row Level Security: tenant_memberships is the first tenant-scoped table. -- Every future tenant-scoped table must repeat this pattern. -- -- Convention: the application sets a session-local Postgres setting -- `app.current_tenant_id` (via set_config(..., true) inside a transaction, -- see packages/database withTenantContext()) before running tenant-scoped -- queries. When unset, current_setting(..., true) returns NULL, and the -- comparison below evaluates to NULL/false — deny-by-default, no data leaks. -- -- FORCE ROW LEVEL SECURITY makes the policy apply even to the table owner -- (the same role used for migrations), so app code cannot accidentally -- bypass isolation just because it runs as that role. ALTER TABLE "tenant_memberships" ENABLE ROW LEVEL SECURITY; ALTER TABLE "tenant_memberships" FORCE ROW LEVEL SECURITY; CREATE POLICY "tenant_isolation" ON "tenant_memberships" USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);