@digimuza/saas-core 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +1165 -0
- package/LICENSE +21 -0
- package/README.md +234 -0
- package/dist/api-keys/index.d.ts +9 -0
- package/dist/api-keys/index.d.ts.map +1 -0
- package/dist/api-keys/index.js +37 -0
- package/dist/api-keys/index.js.map +1 -0
- package/dist/api-keys/keys.d.ts +55 -0
- package/dist/api-keys/keys.d.ts.map +1 -0
- package/dist/api-keys/keys.js +92 -0
- package/dist/api-keys/keys.js.map +1 -0
- package/dist/api-keys/service.d.ts +111 -0
- package/dist/api-keys/service.d.ts.map +1 -0
- package/dist/api-keys/service.js +210 -0
- package/dist/api-keys/service.js.map +1 -0
- package/dist/audit/index.d.ts +5 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +44 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/service.d.ts +88 -0
- package/dist/audit/service.d.ts.map +1 -0
- package/dist/audit/service.js +128 -0
- package/dist/audit/service.js.map +1 -0
- package/dist/auth/client.d.ts +5 -0
- package/dist/auth/client.d.ts.map +1 -0
- package/dist/auth/client.js +26 -0
- package/dist/auth/client.js.map +1 -0
- package/dist/auth/cookies.d.ts +51 -0
- package/dist/auth/cookies.d.ts.map +1 -0
- package/dist/auth/cookies.js +58 -0
- package/dist/auth/cookies.js.map +1 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +46 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/service.d.ts +67 -0
- package/dist/auth/service.d.ts.map +1 -0
- package/dist/auth/service.js +275 -0
- package/dist/auth/service.js.map +1 -0
- package/dist/auth/types.d.ts +56 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/authorization/index.d.ts +4 -0
- package/dist/authorization/index.d.ts.map +1 -0
- package/dist/authorization/index.js +24 -0
- package/dist/authorization/index.js.map +1 -0
- package/dist/authorization/service.d.ts +92 -0
- package/dist/authorization/service.d.ts.map +1 -0
- package/dist/authorization/service.js +254 -0
- package/dist/authorization/service.js.map +1 -0
- package/dist/billing/index.d.ts +16 -0
- package/dist/billing/index.d.ts.map +1 -0
- package/dist/billing/index.js +47 -0
- package/dist/billing/index.js.map +1 -0
- package/dist/billing/provider.d.ts +114 -0
- package/dist/billing/provider.d.ts.map +1 -0
- package/dist/billing/provider.js +2 -0
- package/dist/billing/provider.js.map +1 -0
- package/dist/billing/revolut.d.ts +68 -0
- package/dist/billing/revolut.d.ts.map +1 -0
- package/dist/billing/revolut.js +181 -0
- package/dist/billing/revolut.js.map +1 -0
- package/dist/billing/service.d.ts +124 -0
- package/dist/billing/service.d.ts.map +1 -0
- package/dist/billing/service.js +470 -0
- package/dist/billing/service.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +43 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +212 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/project.d.ts +12 -0
- package/dist/cli/commands/project.d.ts.map +1 -0
- package/dist/cli/commands/project.js +49 -0
- package/dist/cli/commands/project.js.map +1 -0
- package/dist/cli/docker.d.ts +50 -0
- package/dist/cli/docker.d.ts.map +1 -0
- package/dist/cli/docker.js +84 -0
- package/dist/cli/docker.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +198 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output.d.ts +30 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +66 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/templates.d.ts +52 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +225 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/config/client.d.ts +60 -0
- package/dist/config/client.d.ts.map +1 -0
- package/dist/config/client.js +94 -0
- package/dist/config/client.js.map +1 -0
- package/dist/config/config-loader.d.ts +2 -0
- package/dist/config/config-loader.d.ts.map +1 -0
- package/dist/config/config-loader.js +2 -0
- package/dist/config/config-loader.js.map +1 -0
- package/dist/config/env.d.ts +24 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +40 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/errors.d.ts +17 -0
- package/dist/config/errors.d.ts.map +1 -0
- package/dist/config/errors.js +26 -0
- package/dist/config/errors.js.map +1 -0
- package/dist/config/index.d.ts +72 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +106 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/runtime.d.ts +15 -0
- package/dist/config/runtime.d.ts.map +1 -0
- package/dist/config/runtime.js +22 -0
- package/dist/config/runtime.js.map +1 -0
- package/dist/config/sections.d.ts +226 -0
- package/dist/config/sections.d.ts.map +1 -0
- package/dist/config/sections.js +141 -0
- package/dist/config/sections.js.map +1 -0
- package/dist/db/client.d.ts +43 -0
- package/dist/db/client.d.ts.map +1 -0
- package/dist/db/client.js +77 -0
- package/dist/db/client.js.map +1 -0
- package/dist/db/errors.d.ts +39 -0
- package/dist/db/errors.d.ts.map +1 -0
- package/dist/db/errors.js +67 -0
- package/dist/db/errors.js.map +1 -0
- package/dist/db/index.d.ts +10 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +10 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +21 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +28 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/schema/_shared.d.ts +30 -0
- package/dist/db/schema/_shared.d.ts.map +1 -0
- package/dist/db/schema/_shared.js +36 -0
- package/dist/db/schema/_shared.js.map +1 -0
- package/dist/db/schema/apikeys.d.ts +263 -0
- package/dist/db/schema/apikeys.d.ts.map +1 -0
- package/dist/db/schema/apikeys.js +70 -0
- package/dist/db/schema/apikeys.js.map +1 -0
- package/dist/db/schema/authorization.d.ts +465 -0
- package/dist/db/schema/authorization.d.ts.map +1 -0
- package/dist/db/schema/authorization.js +134 -0
- package/dist/db/schema/authorization.js.map +1 -0
- package/dist/db/schema/billing.d.ts +1100 -0
- package/dist/db/schema/billing.d.ts.map +1 -0
- package/dist/db/schema/billing.js +223 -0
- package/dist/db/schema/billing.js.map +1 -0
- package/dist/db/schema/events.d.ts +161 -0
- package/dist/db/schema/events.d.ts.map +1 -0
- package/dist/db/schema/events.js +66 -0
- package/dist/db/schema/events.js.map +1 -0
- package/dist/db/schema/features.d.ts +243 -0
- package/dist/db/schema/features.d.ts.map +1 -0
- package/dist/db/schema/features.js +87 -0
- package/dist/db/schema/features.js.map +1 -0
- package/dist/db/schema/files.d.ts +253 -0
- package/dist/db/schema/files.d.ts.map +1 -0
- package/dist/db/schema/files.js +74 -0
- package/dist/db/schema/files.js.map +1 -0
- package/dist/db/schema/index.d.ts +38 -0
- package/dist/db/schema/index.d.ts.map +1 -0
- package/dist/db/schema/index.js +38 -0
- package/dist/db/schema/index.js.map +1 -0
- package/dist/db/schema/invitations.d.ts +214 -0
- package/dist/db/schema/invitations.d.ts.map +1 -0
- package/dist/db/schema/invitations.js +80 -0
- package/dist/db/schema/invitations.js.map +1 -0
- package/dist/db/schema/messaging.d.ts +442 -0
- package/dist/db/schema/messaging.d.ts.map +1 -0
- package/dist/db/schema/messaging.js +120 -0
- package/dist/db/schema/messaging.js.map +1 -0
- package/dist/db/schema/users.d.ts +246 -0
- package/dist/db/schema/users.d.ts.map +1 -0
- package/dist/db/schema/users.js +60 -0
- package/dist/db/schema/users.js.map +1 -0
- package/dist/db/schema/webhooks.d.ts +693 -0
- package/dist/db/schema/webhooks.d.ts.map +1 -0
- package/dist/db/schema/webhooks.js +147 -0
- package/dist/db/schema/webhooks.js.map +1 -0
- package/dist/db/schema/workspaces.d.ts +196 -0
- package/dist/db/schema/workspaces.d.ts.map +1 -0
- package/dist/db/schema/workspaces.js +57 -0
- package/dist/db/schema/workspaces.js.map +1 -0
- package/dist/db/types.d.ts +18 -0
- package/dist/db/types.d.ts.map +1 -0
- package/dist/db/types.js +2 -0
- package/dist/db/types.js.map +1 -0
- package/dist/email/adapters.d.ts +29 -0
- package/dist/email/adapters.d.ts.map +1 -0
- package/dist/email/adapters.js +60 -0
- package/dist/email/adapters.js.map +1 -0
- package/dist/email/index.d.ts +20 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +79 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/provider.d.ts +47 -0
- package/dist/email/provider.d.ts.map +1 -0
- package/dist/email/provider.js +7 -0
- package/dist/email/provider.js.map +1 -0
- package/dist/email/resend.d.ts +22 -0
- package/dist/email/resend.d.ts.map +1 -0
- package/dist/email/resend.js +51 -0
- package/dist/email/resend.js.map +1 -0
- package/dist/email/service.d.ts +55 -0
- package/dist/email/service.d.ts.map +1 -0
- package/dist/email/service.js +87 -0
- package/dist/email/service.js.map +1 -0
- package/dist/email/templates.d.ts +81 -0
- package/dist/email/templates.d.ts.map +1 -0
- package/dist/email/templates.js +84 -0
- package/dist/email/templates.js.map +1 -0
- package/dist/entitlements/index.d.ts +10 -0
- package/dist/entitlements/index.d.ts.map +1 -0
- package/dist/entitlements/index.js +28 -0
- package/dist/entitlements/index.js.map +1 -0
- package/dist/entitlements/service.d.ts +83 -0
- package/dist/entitlements/service.d.ts.map +1 -0
- package/dist/entitlements/service.js +158 -0
- package/dist/entitlements/service.js.map +1 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +95 -0
- package/dist/errors.js.map +1 -0
- package/dist/events/core-events.d.ts +120 -0
- package/dist/events/core-events.d.ts.map +1 -0
- package/dist/events/core-events.js +109 -0
- package/dist/events/core-events.js.map +1 -0
- package/dist/events/index.d.ts +15 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/index.js +58 -0
- package/dist/events/index.js.map +1 -0
- package/dist/events/publisher.d.ts +46 -0
- package/dist/events/publisher.d.ts.map +1 -0
- package/dist/events/publisher.js +54 -0
- package/dist/events/publisher.js.map +1 -0
- package/dist/events/registry.d.ts +87 -0
- package/dist/events/registry.d.ts.map +1 -0
- package/dist/events/registry.js +34 -0
- package/dist/events/registry.js.map +1 -0
- package/dist/events/service.d.ts +78 -0
- package/dist/events/service.d.ts.map +1 -0
- package/dist/events/service.js +182 -0
- package/dist/events/service.js.map +1 -0
- package/dist/features/bucket.d.ts +22 -0
- package/dist/features/bucket.d.ts.map +1 -0
- package/dist/features/bucket.js +42 -0
- package/dist/features/bucket.js.map +1 -0
- package/dist/features/index.d.ts +15 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +78 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/registry.d.ts +51 -0
- package/dist/features/registry.d.ts.map +1 -0
- package/dist/features/registry.js +52 -0
- package/dist/features/registry.js.map +1 -0
- package/dist/features/service.d.ts +78 -0
- package/dist/features/service.d.ts.map +1 -0
- package/dist/features/service.js +207 -0
- package/dist/features/service.js.map +1 -0
- package/dist/files/index.d.ts +6 -0
- package/dist/files/index.d.ts.map +1 -0
- package/dist/files/index.js +64 -0
- package/dist/files/index.js.map +1 -0
- package/dist/files/service.d.ts +92 -0
- package/dist/files/service.d.ts.map +1 -0
- package/dist/files/service.js +303 -0
- package/dist/files/service.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs/backend.d.ts +23 -0
- package/dist/jobs/backend.d.ts.map +1 -0
- package/dist/jobs/backend.js +23 -0
- package/dist/jobs/backend.js.map +1 -0
- package/dist/jobs/index.d.ts +29 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +114 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/service.d.ts +67 -0
- package/dist/jobs/service.d.ts.map +1 -0
- package/dist/jobs/service.js +139 -0
- package/dist/jobs/service.js.map +1 -0
- package/dist/jobs/types.d.ts +78 -0
- package/dist/jobs/types.d.ts.map +1 -0
- package/dist/jobs/types.js +4 -0
- package/dist/jobs/types.js.map +1 -0
- package/dist/logger/default.d.ts +7 -0
- package/dist/logger/default.d.ts.map +1 -0
- package/dist/logger/default.js +28 -0
- package/dist/logger/default.js.map +1 -0
- package/dist/logger/index.d.ts +24 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +90 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/redact.d.ts +40 -0
- package/dist/logger/redact.d.ts.map +1 -0
- package/dist/logger/redact.js +178 -0
- package/dist/logger/redact.js.map +1 -0
- package/dist/logger/service.d.ts +14 -0
- package/dist/logger/service.d.ts.map +1 -0
- package/dist/logger/service.js +56 -0
- package/dist/logger/service.js.map +1 -0
- package/dist/logger/sinks.d.ts +37 -0
- package/dist/logger/sinks.d.ts.map +1 -0
- package/dist/logger/sinks.js +135 -0
- package/dist/logger/sinks.js.map +1 -0
- package/dist/logger/types.d.ts +47 -0
- package/dist/logger/types.d.ts.map +1 -0
- package/dist/logger/types.js +8 -0
- package/dist/logger/types.js.map +1 -0
- package/dist/notifications/index.d.ts +8 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +72 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/service.d.ts +79 -0
- package/dist/notifications/service.d.ts.map +1 -0
- package/dist/notifications/service.js +164 -0
- package/dist/notifications/service.js.map +1 -0
- package/dist/realtime/channels.d.ts +37 -0
- package/dist/realtime/channels.d.ts.map +1 -0
- package/dist/realtime/channels.js +58 -0
- package/dist/realtime/channels.js.map +1 -0
- package/dist/realtime/index.d.ts +19 -0
- package/dist/realtime/index.d.ts.map +1 -0
- package/dist/realtime/index.js +118 -0
- package/dist/realtime/index.js.map +1 -0
- package/dist/realtime/postgres.d.ts +48 -0
- package/dist/realtime/postgres.d.ts.map +1 -0
- package/dist/realtime/postgres.js +151 -0
- package/dist/realtime/postgres.js.map +1 -0
- package/dist/realtime/service.d.ts +75 -0
- package/dist/realtime/service.d.ts.map +1 -0
- package/dist/realtime/service.js +103 -0
- package/dist/realtime/service.js.map +1 -0
- package/dist/realtime/sse.d.ts +55 -0
- package/dist/realtime/sse.d.ts.map +1 -0
- package/dist/realtime/sse.js +123 -0
- package/dist/realtime/sse.js.map +1 -0
- package/dist/realtime/transport.d.ts +52 -0
- package/dist/realtime/transport.d.ts.map +1 -0
- package/dist/realtime/transport.js +2 -0
- package/dist/realtime/transport.js.map +1 -0
- package/dist/storage/index.d.ts +18 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +89 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/keys.d.ts +50 -0
- package/dist/storage/keys.d.ts.map +1 -0
- package/dist/storage/keys.js +120 -0
- package/dist/storage/keys.js.map +1 -0
- package/dist/storage/memory.d.ts +32 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +84 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/provider.d.ts +93 -0
- package/dist/storage/provider.d.ts.map +1 -0
- package/dist/storage/provider.js +2 -0
- package/dist/storage/provider.js.map +1 -0
- package/dist/storage/s3.d.ts +34 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +175 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/service.d.ts +86 -0
- package/dist/storage/service.d.ts.map +1 -0
- package/dist/storage/service.js +107 -0
- package/dist/storage/service.js.map +1 -0
- package/dist/storage/sigv4.d.ts +58 -0
- package/dist/storage/sigv4.d.ts.map +1 -0
- package/dist/storage/sigv4.js +114 -0
- package/dist/storage/sigv4.js.map +1 -0
- package/dist/streams/client.d.ts +91 -0
- package/dist/streams/client.d.ts.map +1 -0
- package/dist/streams/client.js +128 -0
- package/dist/streams/client.js.map +1 -0
- package/dist/streams/events.d.ts +68 -0
- package/dist/streams/events.d.ts.map +1 -0
- package/dist/streams/events.js +81 -0
- package/dist/streams/events.js.map +1 -0
- package/dist/streams/index.d.ts +47 -0
- package/dist/streams/index.d.ts.map +1 -0
- package/dist/streams/index.js +47 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams/realtime.d.ts +27 -0
- package/dist/streams/realtime.d.ts.map +1 -0
- package/dist/streams/realtime.js +50 -0
- package/dist/streams/realtime.js.map +1 -0
- package/dist/streams/state.d.ts +26 -0
- package/dist/streams/state.d.ts.map +1 -0
- package/dist/streams/state.js +34 -0
- package/dist/streams/state.js.map +1 -0
- package/dist/users/index.d.ts +9 -0
- package/dist/users/index.d.ts.map +1 -0
- package/dist/users/index.js +23 -0
- package/dist/users/index.js.map +1 -0
- package/dist/users/service.d.ts +43 -0
- package/dist/users/service.d.ts.map +1 -0
- package/dist/users/service.js +113 -0
- package/dist/users/service.js.map +1 -0
- package/dist/webhooks/inbound.d.ts +70 -0
- package/dist/webhooks/inbound.d.ts.map +1 -0
- package/dist/webhooks/inbound.js +146 -0
- package/dist/webhooks/inbound.js.map +1 -0
- package/dist/webhooks/index.d.ts +12 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +65 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/outgoing.d.ts +89 -0
- package/dist/webhooks/outgoing.d.ts.map +1 -0
- package/dist/webhooks/outgoing.js +375 -0
- package/dist/webhooks/outgoing.js.map +1 -0
- package/dist/webhooks/signature.d.ts +40 -0
- package/dist/webhooks/signature.d.ts.map +1 -0
- package/dist/webhooks/signature.js +77 -0
- package/dist/webhooks/signature.js.map +1 -0
- package/dist/workspaces/context.d.ts +30 -0
- package/dist/workspaces/context.d.ts.map +1 -0
- package/dist/workspaces/context.js +12 -0
- package/dist/workspaces/context.js.map +1 -0
- package/dist/workspaces/index.d.ts +5 -0
- package/dist/workspaces/index.d.ts.map +1 -0
- package/dist/workspaces/index.js +21 -0
- package/dist/workspaces/index.js.map +1 -0
- package/dist/workspaces/service.d.ts +126 -0
- package/dist/workspaces/service.d.ts.map +1 -0
- package/dist/workspaces/service.js +495 -0
- package/dist/workspaces/service.js.map +1 -0
- package/docs/README.md +71 -0
- package/docs/api-keys.md +184 -0
- package/docs/audit.md +198 -0
- package/docs/auth.md +196 -0
- package/docs/authorization.md +225 -0
- package/docs/billing.md +293 -0
- package/docs/cli.md +238 -0
- package/docs/config.md +163 -0
- package/docs/db.md +219 -0
- package/docs/email.md +222 -0
- package/docs/entitlements.md +194 -0
- package/docs/errors.md +158 -0
- package/docs/events.md +279 -0
- package/docs/feature-flags.md +237 -0
- package/docs/files.md +229 -0
- package/docs/jobs.md +288 -0
- package/docs/logger.md +269 -0
- package/docs/notifications.md +179 -0
- package/docs/publishing.md +128 -0
- package/docs/realtime.md +360 -0
- package/docs/storage.md +279 -0
- package/docs/streams.md +296 -0
- package/docs/users.md +117 -0
- package/docs/webhooks.md +272 -0
- package/docs/workspaces.md +269 -0
- package/drizzle/0000_init_users.sql +21 -0
- package/drizzle/0001_workspaces_roles_permissions.sql +86 -0
- package/drizzle/0002_billing_entitlements.sql +97 -0
- package/drizzle/0003_events.sql +17 -0
- package/drizzle/0004_notifications_audit.sql +64 -0
- package/drizzle/0005_api_keys_webhooks.sql +77 -0
- package/drizzle/0006_feature_flags.sql +22 -0
- package/drizzle/0007_files.sql +23 -0
- package/drizzle/meta/0000_snapshot.json +160 -0
- package/drizzle/meta/0001_snapshot.json +837 -0
- package/drizzle/meta/0002_snapshot.json +1580 -0
- package/drizzle/meta/0003_snapshot.json +1751 -0
- package/drizzle/meta/0004_snapshot.json +2109 -0
- package/drizzle/meta/0005_snapshot.json +2719 -0
- package/drizzle/meta/0006_snapshot.json +2909 -0
- package/drizzle/meta/0007_snapshot.json +3113 -0
- package/drizzle/meta/_journal.json +62 -0
- package/package.json +180 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Workspaces, members & invitations
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { workspaces } from "@digimuza/saas-core/workspaces";
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
> Orientation and recipes: [`src/workspaces/README.md`](../src/workspaces/README.md).
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
A workspace is the tenancy boundary. Users belong to many workspaces through
|
|
11
|
+
`workspace_members`; `workspace_id` never appears on `users`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## `WorkspaceContext`
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface WorkspaceContext {
|
|
19
|
+
readonly userId: string;
|
|
20
|
+
readonly workspaceId: string;
|
|
21
|
+
readonly membershipVerified: true;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Every tenant-scoped operation in the core takes one of these**, and the only way
|
|
26
|
+
to get one is a verified membership lookup. So a `workspaceId` read from a request
|
|
27
|
+
body, query string or path parameter will not type-check where a context is
|
|
28
|
+
required.
|
|
29
|
+
|
|
30
|
+
The marker is a convention, not a cryptographic guarantee — but it makes bypassing
|
|
31
|
+
the check a deliberate, greppable act rather than an oversight.
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
// The standard shape of a tenant-scoped handler.
|
|
35
|
+
const user = await auth.requireUser();
|
|
36
|
+
const context = await workspaces.requireContext({
|
|
37
|
+
userId: user.id,
|
|
38
|
+
workspaceId: params.workspaceId, // untrusted until this call returns
|
|
39
|
+
});
|
|
40
|
+
await authorization.require(context, "projects.create");
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## `workspaces.resolveContext(input)`
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
function resolveContext(input: { userId: string; workspaceId: string }):
|
|
47
|
+
Promise<WorkspaceContext | undefined>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Returns `undefined` when the user is not a member.
|
|
51
|
+
|
|
52
|
+
## `workspaces.requireContext(input)`
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
function requireContext(input: { userId: string; workspaceId: string }): Promise<WorkspaceContext>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Throws **`NotFoundError`**, not `ForbiddenError`. A 403 would confirm that a given
|
|
59
|
+
workspace id exists, letting an attacker enumerate tenants by iterating ids. See
|
|
60
|
+
[errors.md](./errors.md#not-found-vs-forbidden).
|
|
61
|
+
|
|
62
|
+
## `workspaces.requireContextBySlug(input)`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
function requireContextBySlug(input: { userId: string; slug: string }): Promise<WorkspaceContext>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Same guarantee for URL-addressed workspaces. Resolves slug and membership in one
|
|
69
|
+
query, so a non-member cannot distinguish "no such slug" from "not your workspace".
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## `workspaces.create(input)`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
function create(input: {
|
|
77
|
+
name: string;
|
|
78
|
+
slug?: string; // derived from name if omitted
|
|
79
|
+
ownerUserId: string;
|
|
80
|
+
systemRoles?: readonly SystemRoleDefinition[]; // defaults to admin + member
|
|
81
|
+
}): Promise<Workspace>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
In one transaction: creates the workspace, seeds the system roles, adds the creator
|
|
85
|
+
as a member, and grants them `admin`. Without that last step a new workspace would
|
|
86
|
+
have nobody able to administer it.
|
|
87
|
+
|
|
88
|
+
Throws `ConflictError` if the slug is taken, `InvalidInputError` if the name
|
|
89
|
+
contains no alphanumeric character.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const workspace = await workspaces.create({
|
|
93
|
+
name: "Acme Corp", // slug becomes "acme-corp"
|
|
94
|
+
ownerUserId: user.id,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Overriding the seeded roles
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
await workspaces.create({
|
|
102
|
+
name: "Acme",
|
|
103
|
+
ownerUserId: user.id,
|
|
104
|
+
systemRoles: [
|
|
105
|
+
{ key: "admin", name: "Administrator" },
|
|
106
|
+
{ key: "editor", name: "Editor", isDefault: true },
|
|
107
|
+
{ key: "viewer", name: "Viewer" },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The creator gets `admin` if it exists, otherwise the default role, otherwise the
|
|
113
|
+
first. Exactly one role may be `isDefault` per workspace — enforced by a partial
|
|
114
|
+
unique index.
|
|
115
|
+
|
|
116
|
+
## `workspaces.get` / `update` / `delete`
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
function get(context: WorkspaceContext): Promise<Workspace>
|
|
120
|
+
function update(context: WorkspaceContext, patch: { name?: string; slug?: string }): Promise<Workspace>
|
|
121
|
+
function delete(context: WorkspaceContext): Promise<void>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`update` emits `workspace.updated` listing which fields changed, so consumers can
|
|
125
|
+
skip work they do not care about.
|
|
126
|
+
|
|
127
|
+
## `workspaces.listForUser(userId)`
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
function listForUser(userId: string): Promise<Workspace[]>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Takes a raw user id rather than a context — this is the query that *builds* the
|
|
134
|
+
workspace switcher, before any workspace is chosen.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Members
|
|
139
|
+
|
|
140
|
+
### `workspaces.members.list(context)`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
function list(context: WorkspaceContext): Promise<WorkspaceMemberSummary[]>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
interface WorkspaceMemberSummary {
|
|
148
|
+
userId: string;
|
|
149
|
+
email: string | null;
|
|
150
|
+
displayName: string | null;
|
|
151
|
+
avatarUrl: string | null;
|
|
152
|
+
joinedAt: Date;
|
|
153
|
+
roles: readonly { id: string; key: string; name: string }[];
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
One query returning member/role pairs, folded into one entry per member.
|
|
158
|
+
|
|
159
|
+
### `workspaces.members.add(context, input)`
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
function add(context: WorkspaceContext, input: {
|
|
163
|
+
userId: string;
|
|
164
|
+
roleIds?: readonly string[]; // defaults to the workspace default role
|
|
165
|
+
}): Promise<void>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Direct addition, bypassing the invitation flow — for admin tooling and for users
|
|
169
|
+
who already exist. Idempotent.
|
|
170
|
+
|
|
171
|
+
### `workspaces.members.remove(context, userId)`
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
function remove(context: WorkspaceContext, userId: string): Promise<void>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Deletes first, then counts what is left, inside one transaction. Throws
|
|
178
|
+
`ConflictError` if that would empty the workspace, and `NotFoundError` if the user
|
|
179
|
+
is not a member of *this* workspace.
|
|
180
|
+
|
|
181
|
+
The order matters. Counting first was a real bug: it reported "last member" for
|
|
182
|
+
someone who was not a member at all — leaking the workspace's member count to an
|
|
183
|
+
outsider — and it raced, since two concurrent removals could each see two members
|
|
184
|
+
and both proceed.
|
|
185
|
+
|
|
186
|
+
Role grants cascade away via `workspace_member_roles_member_fk`.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Invitations
|
|
191
|
+
|
|
192
|
+
### `workspaces.members.invite(context, input)`
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
function invite(context: WorkspaceContext, input: {
|
|
196
|
+
email: string;
|
|
197
|
+
roleId?: string; // falls back to the workspace default role
|
|
198
|
+
expiresInSeconds?: number; // 7 days
|
|
199
|
+
}): Promise<{ invitation: WorkspaceInvitation; token: string }>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const { invitation, token } = await workspaces.members.invite(context, {
|
|
204
|
+
email: "new@example.com",
|
|
205
|
+
});
|
|
206
|
+
await email.send({ to: invitation.email, token }); // the only chance to use it
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**`token` is returned once.** Only its SHA-256 hash is stored, so a database leak
|
|
210
|
+
does not let an attacker accept outstanding invitations. Lose the token and you
|
|
211
|
+
must revoke and re-issue.
|
|
212
|
+
|
|
213
|
+
Throws `ConflictError` if an invitation for that address is already outstanding —
|
|
214
|
+
a partial unique index allows one *pending* invitation per email per workspace
|
|
215
|
+
while keeping accepted and revoked history queryable. Email is lowercased so the
|
|
216
|
+
index behaves predictably.
|
|
217
|
+
|
|
218
|
+
Throws `InvalidInputError` for a malformed address.
|
|
219
|
+
|
|
220
|
+
### `workspaces.members.acceptInvitation(input)`
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
function acceptInvitation(input: { token: string; userId: string }): Promise<WorkspaceContext>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Not workspace-scoped: the token identifies the workspace. `userId` must be the
|
|
227
|
+
**authenticated** user, never taken from the invitation payload.
|
|
228
|
+
|
|
229
|
+
Returns a verified context, so the caller can immediately act as the new member.
|
|
230
|
+
|
|
231
|
+
Missing, expired, revoked and already-accepted tokens all raise the **same**
|
|
232
|
+
`NotFoundError` with the same message. Distinguishing them would tell a
|
|
233
|
+
token-guessing attacker which guesses corresponded to real invitations.
|
|
234
|
+
|
|
235
|
+
Acceptance re-checks the guard in its `UPDATE ... WHERE`, so two concurrent
|
|
236
|
+
redemptions cannot both succeed.
|
|
237
|
+
|
|
238
|
+
### `listInvitations` / `revokeInvitation`
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
function listInvitations(context: WorkspaceContext): Promise<WorkspaceInvitation[]>
|
|
242
|
+
function revokeInvitation(context: WorkspaceContext, invitationId: string): Promise<void>
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
`listInvitations` returns only outstanding ones. `revokeInvitation` is scoped to the
|
|
246
|
+
caller's workspace, so an id from another tenant matches nothing and raises
|
|
247
|
+
`NotFoundError`.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## `slugify(value)`
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
function slugify(value: string): string
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Normalises Unicode, strips diacritics, lowercases, collapses non-alphanumerics to
|
|
258
|
+
hyphens, caps at 60 characters. Throws `InvalidInputError` if nothing usable
|
|
259
|
+
remains.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Custom construction
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { createWorkspacesService } from "@digimuza/saas-core/workspaces";
|
|
267
|
+
|
|
268
|
+
const workspaces = createWorkspacesService({ db, events }); // events optional
|
|
269
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
CREATE TABLE "user_profiles" (
|
|
2
|
+
"user_id" uuid PRIMARY KEY NOT NULL,
|
|
3
|
+
"email" text NOT NULL,
|
|
4
|
+
"first_name" text,
|
|
5
|
+
"last_name" text,
|
|
6
|
+
"display_name" text,
|
|
7
|
+
"avatar_url" text,
|
|
8
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
9
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
10
|
+
);
|
|
11
|
+
--> statement-breakpoint
|
|
12
|
+
CREATE TABLE "users" (
|
|
13
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
14
|
+
"workos_user_id" text NOT NULL,
|
|
15
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
16
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
17
|
+
CONSTRAINT "users_workos_user_id_unique" UNIQUE("workos_user_id")
|
|
18
|
+
);
|
|
19
|
+
--> statement-breakpoint
|
|
20
|
+
ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
21
|
+
CREATE INDEX "user_profiles_email_lower_idx" ON "user_profiles" USING btree (lower("email"));
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
CREATE TABLE "workspace_members" (
|
|
2
|
+
"workspace_id" uuid NOT NULL,
|
|
3
|
+
"user_id" uuid NOT NULL,
|
|
4
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
5
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
6
|
+
CONSTRAINT "workspace_members_workspace_id_user_id_pk" PRIMARY KEY("workspace_id","user_id")
|
|
7
|
+
);
|
|
8
|
+
--> statement-breakpoint
|
|
9
|
+
CREATE TABLE "workspaces" (
|
|
10
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
11
|
+
"name" text NOT NULL,
|
|
12
|
+
"slug" text NOT NULL,
|
|
13
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
14
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
15
|
+
CONSTRAINT "workspaces_slug_unique" UNIQUE("slug")
|
|
16
|
+
);
|
|
17
|
+
--> statement-breakpoint
|
|
18
|
+
CREATE TABLE "permissions" (
|
|
19
|
+
"key" text PRIMARY KEY NOT NULL,
|
|
20
|
+
"description" text,
|
|
21
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
22
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
23
|
+
);
|
|
24
|
+
--> statement-breakpoint
|
|
25
|
+
CREATE TABLE "role_permissions" (
|
|
26
|
+
"role_id" uuid NOT NULL,
|
|
27
|
+
"permission_key" text NOT NULL,
|
|
28
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
29
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
30
|
+
CONSTRAINT "role_permissions_role_id_permission_key_pk" PRIMARY KEY("role_id","permission_key")
|
|
31
|
+
);
|
|
32
|
+
--> statement-breakpoint
|
|
33
|
+
CREATE TABLE "roles" (
|
|
34
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
35
|
+
"workspace_id" uuid NOT NULL,
|
|
36
|
+
"key" text NOT NULL,
|
|
37
|
+
"name" text NOT NULL,
|
|
38
|
+
"description" text,
|
|
39
|
+
"is_system" boolean DEFAULT false NOT NULL,
|
|
40
|
+
"is_default" boolean DEFAULT false NOT NULL,
|
|
41
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
42
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
43
|
+
CONSTRAINT "roles_id_workspace_id_key" UNIQUE("id","workspace_id")
|
|
44
|
+
);
|
|
45
|
+
--> statement-breakpoint
|
|
46
|
+
CREATE TABLE "workspace_member_roles" (
|
|
47
|
+
"workspace_id" uuid NOT NULL,
|
|
48
|
+
"user_id" uuid NOT NULL,
|
|
49
|
+
"role_id" uuid NOT NULL,
|
|
50
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
51
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
52
|
+
CONSTRAINT "workspace_member_roles_workspace_id_user_id_role_id_pk" PRIMARY KEY("workspace_id","user_id","role_id")
|
|
53
|
+
);
|
|
54
|
+
--> statement-breakpoint
|
|
55
|
+
CREATE TABLE "workspace_invitations" (
|
|
56
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
57
|
+
"workspace_id" uuid NOT NULL,
|
|
58
|
+
"email" text NOT NULL,
|
|
59
|
+
"token_hash" text NOT NULL,
|
|
60
|
+
"invited_by_user_id" uuid,
|
|
61
|
+
"role_id" uuid,
|
|
62
|
+
"expires_at" timestamp with time zone NOT NULL,
|
|
63
|
+
"accepted_at" timestamp with time zone,
|
|
64
|
+
"revoked_at" timestamp with time zone,
|
|
65
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
66
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
67
|
+
CONSTRAINT "workspace_invitations_token_hash_unique" UNIQUE("token_hash")
|
|
68
|
+
);
|
|
69
|
+
--> statement-breakpoint
|
|
70
|
+
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
71
|
+
ALTER TABLE "workspace_members" ADD CONSTRAINT "workspace_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
72
|
+
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_roles_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
73
|
+
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_key_permissions_key_fk" FOREIGN KEY ("permission_key") REFERENCES "public"."permissions"("key") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
74
|
+
ALTER TABLE "roles" ADD CONSTRAINT "roles_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
75
|
+
ALTER TABLE "workspace_member_roles" ADD CONSTRAINT "workspace_member_roles_member_fk" FOREIGN KEY ("workspace_id","user_id") REFERENCES "public"."workspace_members"("workspace_id","user_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
76
|
+
ALTER TABLE "workspace_member_roles" ADD CONSTRAINT "workspace_member_roles_role_fk" FOREIGN KEY ("role_id","workspace_id") REFERENCES "public"."roles"("id","workspace_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
77
|
+
ALTER TABLE "workspace_invitations" ADD CONSTRAINT "workspace_invitations_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
78
|
+
ALTER TABLE "workspace_invitations" ADD CONSTRAINT "workspace_invitations_invited_by_user_id_users_id_fk" FOREIGN KEY ("invited_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
79
|
+
ALTER TABLE "workspace_invitations" ADD CONSTRAINT "workspace_invitations_role_fk" FOREIGN KEY ("role_id","workspace_id") REFERENCES "public"."roles"("id","workspace_id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
80
|
+
CREATE INDEX "workspace_members_user_id_idx" ON "workspace_members" USING btree ("user_id");--> statement-breakpoint
|
|
81
|
+
CREATE INDEX "role_permissions_permission_key_idx" ON "role_permissions" USING btree ("permission_key");--> statement-breakpoint
|
|
82
|
+
CREATE UNIQUE INDEX "roles_workspace_id_key_idx" ON "roles" USING btree ("workspace_id","key");--> statement-breakpoint
|
|
83
|
+
CREATE UNIQUE INDEX "roles_workspace_default_idx" ON "roles" USING btree ("workspace_id") WHERE "roles"."is_default";--> statement-breakpoint
|
|
84
|
+
CREATE INDEX "workspace_member_roles_role_id_idx" ON "workspace_member_roles" USING btree ("role_id");--> statement-breakpoint
|
|
85
|
+
CREATE INDEX "workspace_invitations_workspace_id_idx" ON "workspace_invitations" USING btree ("workspace_id");--> statement-breakpoint
|
|
86
|
+
CREATE UNIQUE INDEX "workspace_invitations_pending_idx" ON "workspace_invitations" USING btree ("workspace_id",lower("email")) WHERE "workspace_invitations"."accepted_at" is null and "workspace_invitations"."revoked_at" is null;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
CREATE TYPE "public"."billing_event_status" AS ENUM('pending', 'processed', 'failed', 'ignored');--> statement-breakpoint
|
|
2
|
+
CREATE TYPE "public"."billing_interval" AS ENUM('month', 'year', 'one_time');--> statement-breakpoint
|
|
3
|
+
CREATE TYPE "public"."entitlement_kind" AS ENUM('feature', 'limit');--> statement-breakpoint
|
|
4
|
+
CREATE TYPE "public"."subscription_status" AS ENUM('trialing', 'active', 'past_due', 'paused', 'canceled', 'incomplete');--> statement-breakpoint
|
|
5
|
+
CREATE TABLE "billing_customers" (
|
|
6
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
7
|
+
"workspace_id" uuid NOT NULL,
|
|
8
|
+
"provider" text NOT NULL,
|
|
9
|
+
"provider_customer_id" text NOT NULL,
|
|
10
|
+
"email" text,
|
|
11
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
12
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
13
|
+
CONSTRAINT "billing_customers_workspace_id_unique" UNIQUE("workspace_id"),
|
|
14
|
+
CONSTRAINT "billing_customers_provider_customer_unique" UNIQUE("provider","provider_customer_id")
|
|
15
|
+
);
|
|
16
|
+
--> statement-breakpoint
|
|
17
|
+
CREATE TABLE "billing_events" (
|
|
18
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
19
|
+
"provider" text NOT NULL,
|
|
20
|
+
"provider_event_id" text NOT NULL,
|
|
21
|
+
"type" text NOT NULL,
|
|
22
|
+
"payload" jsonb NOT NULL,
|
|
23
|
+
"status" "billing_event_status" DEFAULT 'pending' NOT NULL,
|
|
24
|
+
"processed_at" timestamp with time zone,
|
|
25
|
+
"error" text,
|
|
26
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
27
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
28
|
+
CONSTRAINT "billing_events_provider_event_unique" UNIQUE("provider","provider_event_id")
|
|
29
|
+
);
|
|
30
|
+
--> statement-breakpoint
|
|
31
|
+
CREATE TABLE "entitlements" (
|
|
32
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
33
|
+
"plan_id" uuid NOT NULL,
|
|
34
|
+
"key" text NOT NULL,
|
|
35
|
+
"kind" "entitlement_kind" NOT NULL,
|
|
36
|
+
"limit_value" bigint,
|
|
37
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
38
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
39
|
+
CONSTRAINT "entitlements_plan_id_key_unique" UNIQUE("plan_id","key")
|
|
40
|
+
);
|
|
41
|
+
--> statement-breakpoint
|
|
42
|
+
CREATE TABLE "plans" (
|
|
43
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
44
|
+
"key" text NOT NULL,
|
|
45
|
+
"name" text NOT NULL,
|
|
46
|
+
"description" text,
|
|
47
|
+
"provider_price_id" text,
|
|
48
|
+
"amount_cents" integer DEFAULT 0 NOT NULL,
|
|
49
|
+
"currency" text DEFAULT 'EUR' NOT NULL,
|
|
50
|
+
"interval" "billing_interval" DEFAULT 'month' NOT NULL,
|
|
51
|
+
"is_active" boolean DEFAULT true NOT NULL,
|
|
52
|
+
"sort_order" integer DEFAULT 0 NOT NULL,
|
|
53
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
54
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
55
|
+
CONSTRAINT "plans_key_unique" UNIQUE("key")
|
|
56
|
+
);
|
|
57
|
+
--> statement-breakpoint
|
|
58
|
+
CREATE TABLE "subscriptions" (
|
|
59
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
60
|
+
"workspace_id" uuid NOT NULL,
|
|
61
|
+
"billing_customer_id" uuid NOT NULL,
|
|
62
|
+
"plan_id" uuid NOT NULL,
|
|
63
|
+
"provider" text NOT NULL,
|
|
64
|
+
"provider_subscription_id" text NOT NULL,
|
|
65
|
+
"status" "subscription_status" NOT NULL,
|
|
66
|
+
"current_period_start" timestamp with time zone,
|
|
67
|
+
"current_period_end" timestamp with time zone,
|
|
68
|
+
"cancel_at_period_end" boolean DEFAULT false NOT NULL,
|
|
69
|
+
"canceled_at" timestamp with time zone,
|
|
70
|
+
"trial_ends_at" timestamp with time zone,
|
|
71
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
72
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
73
|
+
CONSTRAINT "subscriptions_provider_subscription_unique" UNIQUE("provider","provider_subscription_id")
|
|
74
|
+
);
|
|
75
|
+
--> statement-breakpoint
|
|
76
|
+
CREATE TABLE "usage_records" (
|
|
77
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
78
|
+
"workspace_id" uuid NOT NULL,
|
|
79
|
+
"key" text NOT NULL,
|
|
80
|
+
"quantity" integer DEFAULT 1 NOT NULL,
|
|
81
|
+
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
82
|
+
"idempotency_key" text,
|
|
83
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
84
|
+
);
|
|
85
|
+
--> statement-breakpoint
|
|
86
|
+
ALTER TABLE "billing_customers" ADD CONSTRAINT "billing_customers_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
87
|
+
ALTER TABLE "entitlements" ADD CONSTRAINT "entitlements_plan_id_plans_id_fk" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
88
|
+
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
89
|
+
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_billing_customer_id_billing_customers_id_fk" FOREIGN KEY ("billing_customer_id") REFERENCES "public"."billing_customers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
90
|
+
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_plan_id_plans_id_fk" FOREIGN KEY ("plan_id") REFERENCES "public"."plans"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
|
|
91
|
+
ALTER TABLE "usage_records" ADD CONSTRAINT "usage_records_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
92
|
+
CREATE INDEX "billing_events_status_idx" ON "billing_events" USING btree ("status");--> statement-breakpoint
|
|
93
|
+
CREATE INDEX "plans_is_active_idx" ON "plans" USING btree ("is_active");--> statement-breakpoint
|
|
94
|
+
CREATE INDEX "subscriptions_workspace_id_idx" ON "subscriptions" USING btree ("workspace_id");--> statement-breakpoint
|
|
95
|
+
CREATE UNIQUE INDEX "subscriptions_one_live_per_workspace_idx" ON "subscriptions" USING btree ("workspace_id") WHERE "subscriptions"."status" in ('trialing', 'active', 'past_due');--> statement-breakpoint
|
|
96
|
+
CREATE INDEX "usage_records_workspace_key_occurred_idx" ON "usage_records" USING btree ("workspace_id","key","occurred_at");--> statement-breakpoint
|
|
97
|
+
CREATE UNIQUE INDEX "usage_records_idempotency_idx" ON "usage_records" USING btree ("workspace_id","key","idempotency_key") WHERE "usage_records"."idempotency_key" is not null;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CREATE TABLE "events" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
3
|
+
"type" text NOT NULL,
|
|
4
|
+
"workspace_id" uuid,
|
|
5
|
+
"actor_user_id" uuid,
|
|
6
|
+
"data" jsonb NOT NULL,
|
|
7
|
+
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
8
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
9
|
+
"idempotency_key" text
|
|
10
|
+
);
|
|
11
|
+
--> statement-breakpoint
|
|
12
|
+
ALTER TABLE "events" ADD CONSTRAINT "events_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
13
|
+
ALTER TABLE "events" ADD CONSTRAINT "events_actor_user_id_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
14
|
+
CREATE INDEX "events_workspace_occurred_idx" ON "events" USING btree ("workspace_id","occurred_at" DESC NULLS LAST);--> statement-breakpoint
|
|
15
|
+
CREATE INDEX "events_type_occurred_idx" ON "events" USING btree ("type","occurred_at" DESC NULLS LAST);--> statement-breakpoint
|
|
16
|
+
CREATE INDEX "events_actor_idx" ON "events" USING btree ("actor_user_id");--> statement-breakpoint
|
|
17
|
+
CREATE UNIQUE INDEX "events_idempotency_idx" ON "events" USING btree ("type","idempotency_key") WHERE "events"."idempotency_key" is not null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
CREATE TABLE "audit_logs" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
3
|
+
"actor_user_id" uuid,
|
|
4
|
+
"workspace_id" uuid,
|
|
5
|
+
"action" text NOT NULL,
|
|
6
|
+
"target_type" text,
|
|
7
|
+
"target_id" text,
|
|
8
|
+
"metadata" jsonb,
|
|
9
|
+
"request_id" text,
|
|
10
|
+
"ip_address" text,
|
|
11
|
+
"user_agent" text,
|
|
12
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
13
|
+
);
|
|
14
|
+
--> statement-breakpoint
|
|
15
|
+
CREATE TABLE "notifications" (
|
|
16
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
17
|
+
"user_id" uuid NOT NULL,
|
|
18
|
+
"workspace_id" uuid,
|
|
19
|
+
"type" text NOT NULL,
|
|
20
|
+
"title" text NOT NULL,
|
|
21
|
+
"body" text,
|
|
22
|
+
"url" text,
|
|
23
|
+
"data" jsonb,
|
|
24
|
+
"read_at" timestamp with time zone,
|
|
25
|
+
"dedupe_key" text,
|
|
26
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
27
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
|
28
|
+
);
|
|
29
|
+
--> statement-breakpoint
|
|
30
|
+
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
31
|
+
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
32
|
+
CREATE INDEX "audit_logs_workspace_created_idx" ON "audit_logs" USING btree ("workspace_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
33
|
+
CREATE INDEX "audit_logs_actor_created_idx" ON "audit_logs" USING btree ("actor_user_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
34
|
+
CREATE INDEX "audit_logs_action_created_idx" ON "audit_logs" USING btree ("action","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
35
|
+
CREATE INDEX "audit_logs_target_idx" ON "audit_logs" USING btree ("target_type","target_id");--> statement-breakpoint
|
|
36
|
+
CREATE INDEX "notifications_user_unread_idx" ON "notifications" USING btree ("user_id","read_at","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
37
|
+
CREATE INDEX "notifications_workspace_idx" ON "notifications" USING btree ("workspace_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
38
|
+
CREATE UNIQUE INDEX "notifications_dedupe_idx" ON "notifications" USING btree ("user_id","type","dedupe_key") WHERE "notifications"."dedupe_key" is not null;
|
|
39
|
+
|
|
40
|
+
--> statement-breakpoint
|
|
41
|
+
-- Audit immutability, enforced by PostgreSQL rather than by convention.
|
|
42
|
+
--
|
|
43
|
+
-- The service exposes no update or delete, but a trail that depends on
|
|
44
|
+
-- application discipline is not a trail: a migration, a data repair, or a psql
|
|
45
|
+
-- session would silently rewrite history. This rejects both operations outright,
|
|
46
|
+
-- so amending an entry requires deliberately dropping the trigger first — which
|
|
47
|
+
-- is itself an auditable act.
|
|
48
|
+
--
|
|
49
|
+
-- This is also why audit_logs carries no foreign keys. `ON DELETE SET NULL` is an
|
|
50
|
+
-- UPDATE, so a referencing audit row would make its own subject undeletable.
|
|
51
|
+
CREATE OR REPLACE FUNCTION saas_core_audit_logs_immutable()
|
|
52
|
+
RETURNS trigger AS $$
|
|
53
|
+
BEGIN
|
|
54
|
+
RAISE EXCEPTION
|
|
55
|
+
'audit_logs is append-only: % is not permitted', TG_OP
|
|
56
|
+
USING ERRCODE = 'restrict_violation';
|
|
57
|
+
END;
|
|
58
|
+
$$ LANGUAGE plpgsql;--> statement-breakpoint
|
|
59
|
+
CREATE TRIGGER audit_logs_no_update
|
|
60
|
+
BEFORE UPDATE ON "audit_logs"
|
|
61
|
+
FOR EACH ROW EXECUTE FUNCTION saas_core_audit_logs_immutable();--> statement-breakpoint
|
|
62
|
+
CREATE TRIGGER audit_logs_no_delete
|
|
63
|
+
BEFORE DELETE ON "audit_logs"
|
|
64
|
+
FOR EACH ROW EXECUTE FUNCTION saas_core_audit_logs_immutable();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
CREATE TYPE "public"."inbound_webhook_status" AS ENUM('pending', 'processed', 'failed', 'ignored');--> statement-breakpoint
|
|
2
|
+
CREATE TYPE "public"."webhook_delivery_status" AS ENUM('pending', 'succeeded', 'failed', 'exhausted');--> statement-breakpoint
|
|
3
|
+
CREATE TABLE "api_keys" (
|
|
4
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
5
|
+
"workspace_id" uuid NOT NULL,
|
|
6
|
+
"name" text NOT NULL,
|
|
7
|
+
"key_id" text NOT NULL,
|
|
8
|
+
"key_hash" text NOT NULL,
|
|
9
|
+
"scopes" text[] DEFAULT '{}'::text[] NOT NULL,
|
|
10
|
+
"created_by_user_id" uuid,
|
|
11
|
+
"last_used_at" timestamp with time zone,
|
|
12
|
+
"expires_at" timestamp with time zone,
|
|
13
|
+
"revoked_at" timestamp with time zone,
|
|
14
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
15
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
16
|
+
CONSTRAINT "api_keys_key_id_unique" UNIQUE("key_id")
|
|
17
|
+
);
|
|
18
|
+
--> statement-breakpoint
|
|
19
|
+
CREATE TABLE "inbound_webhooks" (
|
|
20
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
21
|
+
"source" text NOT NULL,
|
|
22
|
+
"external_id" text NOT NULL,
|
|
23
|
+
"type" text NOT NULL,
|
|
24
|
+
"payload" jsonb NOT NULL,
|
|
25
|
+
"workspace_id" uuid,
|
|
26
|
+
"status" "inbound_webhook_status" DEFAULT 'pending' NOT NULL,
|
|
27
|
+
"processed_at" timestamp with time zone,
|
|
28
|
+
"error" text,
|
|
29
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
30
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
31
|
+
CONSTRAINT "inbound_webhooks_source_external_unique" UNIQUE("source","external_id")
|
|
32
|
+
);
|
|
33
|
+
--> statement-breakpoint
|
|
34
|
+
CREATE TABLE "webhook_deliveries" (
|
|
35
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
36
|
+
"endpoint_id" uuid NOT NULL,
|
|
37
|
+
"event_id" uuid NOT NULL,
|
|
38
|
+
"event_type" text NOT NULL,
|
|
39
|
+
"payload" jsonb NOT NULL,
|
|
40
|
+
"status" "webhook_delivery_status" DEFAULT 'pending' NOT NULL,
|
|
41
|
+
"attempt_count" integer DEFAULT 0 NOT NULL,
|
|
42
|
+
"next_attempt_at" timestamp with time zone,
|
|
43
|
+
"last_attempted_at" timestamp with time zone,
|
|
44
|
+
"response_status" integer,
|
|
45
|
+
"error" text,
|
|
46
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
47
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
48
|
+
CONSTRAINT "webhook_deliveries_endpoint_event_unique" UNIQUE("endpoint_id","event_id")
|
|
49
|
+
);
|
|
50
|
+
--> statement-breakpoint
|
|
51
|
+
CREATE TABLE "webhook_endpoints" (
|
|
52
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
53
|
+
"workspace_id" uuid NOT NULL,
|
|
54
|
+
"url" text NOT NULL,
|
|
55
|
+
"description" text,
|
|
56
|
+
"secret" text NOT NULL,
|
|
57
|
+
"event_types" text[] DEFAULT '{}'::text[] NOT NULL,
|
|
58
|
+
"is_active" boolean DEFAULT true NOT NULL,
|
|
59
|
+
"consecutive_failures" integer DEFAULT 0 NOT NULL,
|
|
60
|
+
"disabled_at" timestamp with time zone,
|
|
61
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
62
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
63
|
+
CONSTRAINT "webhook_endpoints_workspace_url_unique" UNIQUE("workspace_id","url")
|
|
64
|
+
);
|
|
65
|
+
--> statement-breakpoint
|
|
66
|
+
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
67
|
+
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
68
|
+
ALTER TABLE "inbound_webhooks" ADD CONSTRAINT "inbound_webhooks_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
69
|
+
ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_endpoint_id_webhook_endpoints_id_fk" FOREIGN KEY ("endpoint_id") REFERENCES "public"."webhook_endpoints"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
70
|
+
ALTER TABLE "webhook_deliveries" ADD CONSTRAINT "webhook_deliveries_event_id_events_id_fk" FOREIGN KEY ("event_id") REFERENCES "public"."events"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
71
|
+
ALTER TABLE "webhook_endpoints" ADD CONSTRAINT "webhook_endpoints_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
72
|
+
CREATE INDEX "api_keys_workspace_idx" ON "api_keys" USING btree ("workspace_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
73
|
+
CREATE INDEX "inbound_webhooks_status_idx" ON "inbound_webhooks" USING btree ("status");--> statement-breakpoint
|
|
74
|
+
CREATE INDEX "inbound_webhooks_source_idx" ON "inbound_webhooks" USING btree ("source","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
75
|
+
CREATE INDEX "webhook_deliveries_due_idx" ON "webhook_deliveries" USING btree ("status","next_attempt_at");--> statement-breakpoint
|
|
76
|
+
CREATE INDEX "webhook_deliveries_endpoint_idx" ON "webhook_deliveries" USING btree ("endpoint_id","created_at" DESC NULLS LAST);--> statement-breakpoint
|
|
77
|
+
CREATE INDEX "webhook_endpoints_workspace_idx" ON "webhook_endpoints" USING btree ("workspace_id");
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
CREATE TYPE "public"."feature_flag_scope" AS ENUM('global', 'application', 'workspace', 'user');--> statement-breakpoint
|
|
2
|
+
CREATE TABLE "feature_flags" (
|
|
3
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
4
|
+
"key" text NOT NULL,
|
|
5
|
+
"scope" "feature_flag_scope" NOT NULL,
|
|
6
|
+
"application" text,
|
|
7
|
+
"workspace_id" uuid,
|
|
8
|
+
"user_id" uuid,
|
|
9
|
+
"enabled" boolean NOT NULL,
|
|
10
|
+
"variant" text,
|
|
11
|
+
"rollout_percentage" integer,
|
|
12
|
+
"description" text,
|
|
13
|
+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
14
|
+
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
|
15
|
+
CONSTRAINT "feature_flags_target_unique" UNIQUE NULLS NOT DISTINCT("key","scope","application","workspace_id","user_id")
|
|
16
|
+
);
|
|
17
|
+
--> statement-breakpoint
|
|
18
|
+
ALTER TABLE "feature_flags" ADD CONSTRAINT "feature_flags_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
19
|
+
ALTER TABLE "feature_flags" ADD CONSTRAINT "feature_flags_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
20
|
+
CREATE INDEX "feature_flags_key_scope_idx" ON "feature_flags" USING btree ("key","scope");--> statement-breakpoint
|
|
21
|
+
CREATE INDEX "feature_flags_workspace_idx" ON "feature_flags" USING btree ("workspace_id");--> statement-breakpoint
|
|
22
|
+
CREATE INDEX "feature_flags_user_idx" ON "feature_flags" USING btree ("user_id");
|