@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
package/docs/cli.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npx sc init
|
|
5
|
+
npx sc start
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
> Orientation and design notes: [`src/cli/README.md`](../src/cli/README.md).
|
|
9
|
+
|
|
10
|
+
A utility CLI, shipped as the `sc` binary, for bootstrapping and running a project
|
|
11
|
+
built on this package. The original `saas-core` binary remains available for the
|
|
12
|
+
advanced development stack.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## `sc init`
|
|
17
|
+
|
|
18
|
+
Prompts for the project name and creates `docker-compose.yml` in the current
|
|
19
|
+
directory. The generated stack contains PostgreSQL 17, binds it to
|
|
20
|
+
`127.0.0.1:5432`, persists its data in a named volume, and includes a healthcheck.
|
|
21
|
+
|
|
22
|
+
Pass `--project <name>` to skip the prompt, `--postgres-port <port>` to select a
|
|
23
|
+
different host port, or `--force` to replace an existing Compose file.
|
|
24
|
+
|
|
25
|
+
## `sc start`
|
|
26
|
+
|
|
27
|
+
Starts the generated Compose stack in the background and waits until PostgreSQL
|
|
28
|
+
is healthy:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
sc start
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Run `sc init` first. The command never searches parent directories, so it starts
|
|
35
|
+
only the Compose file owned by the current project.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## `saas-core dev up`
|
|
40
|
+
|
|
41
|
+
Generates `.saas-core/` if it is missing, starts the stack, and waits for every
|
|
42
|
+
healthcheck to pass before returning.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
> docker compose v2.29.0
|
|
46
|
+
> created ./.saas-core/docker-compose.yml
|
|
47
|
+
> created ./.saas-core/Caddyfile
|
|
48
|
+
> created ./.saas-core/.gitignore
|
|
49
|
+
> starting postgres, minio and caddy
|
|
50
|
+
ok stack up (project my-app)
|
|
51
|
+
|
|
52
|
+
Endpoints
|
|
53
|
+
app https://app.localhost -> localhost:3000
|
|
54
|
+
postgres localhost:5432
|
|
55
|
+
minio http://localhost:9000
|
|
56
|
+
minio console http://localhost:9001
|
|
57
|
+
|
|
58
|
+
Configuration
|
|
59
|
+
Load these through Infisical rather than a .env file:
|
|
60
|
+
|
|
61
|
+
APP_URL https://app.localhost
|
|
62
|
+
DATABASE_URL postgres://saas:saas@localhost:5432/saas_core
|
|
63
|
+
STORAGE_BUCKET saas-core-dev
|
|
64
|
+
STORAGE_REGION us-east-1
|
|
65
|
+
STORAGE_ENDPOINT http://localhost:9000
|
|
66
|
+
STORAGE_ACCESS_KEY_ID saascoredev
|
|
67
|
+
STORAGE_SECRET_ACCESS_KEY saascoredev
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### What it starts, and what it does not
|
|
71
|
+
|
|
72
|
+
| Service | Why |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| PostgreSQL 17 | Durable state, and the realtime transport — `LISTEN`/`NOTIFY` |
|
|
75
|
+
| MinIO | S3-compatible store for [storage](./storage.md) and [files](./files.md) |
|
|
76
|
+
| Caddy | Local HTTPS in front of your dev server |
|
|
77
|
+
|
|
78
|
+
No Redis: [realtime](./realtime.md) runs on PostgreSQL and [jobs](./jobs.md) on
|
|
79
|
+
Trigger.dev, so there is nothing else to operate. No mail catcher: the
|
|
80
|
+
[email](./email.md) module has a console provider for development.
|
|
81
|
+
|
|
82
|
+
### Why Caddy is not optional
|
|
83
|
+
|
|
84
|
+
Three things differ over plain HTTP, and all three stay invisible until deploy:
|
|
85
|
+
|
|
86
|
+
- [`auth`](./auth.md) marks session cookies `Secure` outside development.
|
|
87
|
+
- `SameSite` enforcement differs between schemes.
|
|
88
|
+
- WorkOS redirect URIs are easier to keep consistent when both environments are
|
|
89
|
+
`https`.
|
|
90
|
+
|
|
91
|
+
`tls internal` issues a certificate from Caddy's own CA, so local matches
|
|
92
|
+
production. Trust the root once and the browser stops warning — `up` prints the
|
|
93
|
+
command to extract it.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Commands
|
|
98
|
+
|
|
99
|
+
| Command | Effect |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `dev up` | Generate, start, wait for health |
|
|
102
|
+
| `dev down` | Stop; **data preserved** |
|
|
103
|
+
| `dev down --volumes` | Stop and delete volumes — the one destructive command |
|
|
104
|
+
| `dev status` (`dev ps`) | Container state |
|
|
105
|
+
| `dev logs [service]` | Last 100 lines; `--follow` to stream |
|
|
106
|
+
| `dev init` | Write the files without starting anything |
|
|
107
|
+
|
|
108
|
+
## Options
|
|
109
|
+
|
|
110
|
+
| Option | Default |
|
|
111
|
+
| --- | --- |
|
|
112
|
+
| `--project <name>` | the directory name, normalised |
|
|
113
|
+
| `--domain <host>` | `app.localhost` |
|
|
114
|
+
| `--app-port <n>` | `3000` |
|
|
115
|
+
| `--postgres-port <n>` | `5432` |
|
|
116
|
+
| `--minio-port <n>` | `9000`; the console takes the next port |
|
|
117
|
+
| `--force` | off — overwrite generated files |
|
|
118
|
+
| `--follow`, `-f` | off — follow logs |
|
|
119
|
+
| `--volumes` | off — with `down`, delete volumes |
|
|
120
|
+
| `--help`, `-h` / `--version` | |
|
|
121
|
+
|
|
122
|
+
Ports are validated before Docker sees them, so a typo fails with a message rather
|
|
123
|
+
than a bind error.
|
|
124
|
+
|
|
125
|
+
The project name defaults to the directory name, so two checkouts on one machine
|
|
126
|
+
get separate containers and volumes without anyone configuring it.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## The generated files
|
|
131
|
+
|
|
132
|
+
`.saas-core/docker-compose.yml` and `.saas-core/Caddyfile` are **meant to be
|
|
133
|
+
committed**. They are infrastructure, and a change to either should show up in a
|
|
134
|
+
diff.
|
|
135
|
+
|
|
136
|
+
They are generated rather than vendored because ports collide: a developer with an
|
|
137
|
+
existing PostgreSQL on 5432 needs a different one, and editing a vendored file
|
|
138
|
+
means losing the edit on every upgrade.
|
|
139
|
+
|
|
140
|
+
**Existing files are left alone.** A second `up` will not discard a Caddyfile you
|
|
141
|
+
added a second host to. `--force` overwrites.
|
|
142
|
+
|
|
143
|
+
`.saas-core/.gitignore` ignores only local state — logs — never the two
|
|
144
|
+
infrastructure files.
|
|
145
|
+
|
|
146
|
+
### Choices baked into the compose file
|
|
147
|
+
|
|
148
|
+
**Every published port binds to `127.0.0.1`,** not `0.0.0.0`. Otherwise a laptop
|
|
149
|
+
on a cafe network is serving an unauthenticated database to the room. A test walks
|
|
150
|
+
every port mapping in the generated file and asserts it.
|
|
151
|
+
|
|
152
|
+
**`up --wait`.** Compose blocks on the healthchecks (`pg_isready`, `mc ready`), so
|
|
153
|
+
the command returning means the stack is usable — not merely created. Without it
|
|
154
|
+
the next command races the database.
|
|
155
|
+
|
|
156
|
+
**PostgreSQL pinned to 17; 15 is the floor.** The schema uses `UNIQUE NULLS NOT
|
|
157
|
+
DISTINCT` for [feature flag](./feature-flags.md) overrides, which does not exist
|
|
158
|
+
before 15. Pinning the major keeps a local database from silently diverging from
|
|
159
|
+
production, and a test asserts it.
|
|
160
|
+
|
|
161
|
+
**A one-shot `minio-init`** creates the bucket. MinIO starts with none, so without
|
|
162
|
+
it every upload fails on a fresh volume.
|
|
163
|
+
|
|
164
|
+
**`host.docker.internal:host-gateway`** is mapped explicitly. Caddy proxies to the
|
|
165
|
+
dev server on the *host* — so hot reload and the real `node_modules` keep working —
|
|
166
|
+
and Docker Desktop provides that name while Linux needs the mapping.
|
|
167
|
+
|
|
168
|
+
**Credentials are fixed and weak.** `saas:saas`, `saascoredev`. Throwaway data on
|
|
169
|
+
loopback; a generated password is one more thing to look up before running `psql`.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Configuration is printed, not written
|
|
174
|
+
|
|
175
|
+
There is deliberately no `--write-env`. Configuration comes from Infisical (see
|
|
176
|
+
[config](./config.md)), and a `.env` file sitting in the project is exactly the
|
|
177
|
+
habit that leads to one in production.
|
|
178
|
+
|
|
179
|
+
`up` prints the values and the matching `infisical secrets set` commands, so the
|
|
180
|
+
next step is a paste rather than a lookup.
|
|
181
|
+
|
|
182
|
+
The variable names are checked against `src/config/sections.ts` by a test — a
|
|
183
|
+
drifted name would otherwise be silent: the stack comes up and the application
|
|
184
|
+
cannot see it.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Error handling
|
|
189
|
+
|
|
190
|
+
Docker is checked before anything else, because "command not found", "daemon not
|
|
191
|
+
running" and "no compose plugin" are three problems with three different fixes:
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
x The Docker CLI is installed but its daemon is not reachable.
|
|
195
|
+
Start Docker Desktop, or run `sudo systemctl start docker`.
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**The daemon needs its own probe.** `docker compose version` reports the *client*
|
|
199
|
+
plugin and succeeds with the daemon stopped — so checking only that lets a stopped
|
|
200
|
+
daemon through to fail halfway through an image pull. That was a real bug during
|
|
201
|
+
development; there is a test pinning the two-probe sequence.
|
|
202
|
+
|
|
203
|
+
Compose failures are translated where the cause is knowable — a port collision
|
|
204
|
+
names `--postgres-port`, exhausted disk suggests `docker system prune` — and passed
|
|
205
|
+
through verbatim otherwise. Anything unrecognised is better shown raw than
|
|
206
|
+
mislabelled.
|
|
207
|
+
|
|
208
|
+
Errors print a message and a hint, never a stack trace: a stack tells someone
|
|
209
|
+
running a CLI nothing actionable and buries the one line that matters.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## No CLI dependency
|
|
214
|
+
|
|
215
|
+
Node's `parseArgs` handles arguments; four escape codes and a TTY check handle
|
|
216
|
+
colour. Commander and chalk would each be a dependency for under twenty lines, in
|
|
217
|
+
a package many applications install.
|
|
218
|
+
|
|
219
|
+
Colour is dropped when stdout is not a TTY, when `NO_COLOR` is set, or when `TERM`
|
|
220
|
+
is `dumb` — escape codes in a CI log are noise a human then reads around.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Testing
|
|
225
|
+
|
|
226
|
+
`CommandRunner` is a one-method interface, so every argument list is asserted
|
|
227
|
+
without Docker installed:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
const { runner, calls } = fakeRunner();
|
|
231
|
+
await devUp({ cwd, runner, projectName: "demo" });
|
|
232
|
+
|
|
233
|
+
expect(calls.find((c) => c.args.includes("up"))?.args).toContain("--wait");
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
That covers template generation, port validation, file-preservation behaviour,
|
|
237
|
+
compose argument construction, and every error path. What it cannot cover is a real
|
|
238
|
+
container coming up — see the note in the root README.
|
package/docs/config.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { config, assertConfig, ConfigError } from "@digimuza/saas-core/config";
|
|
5
|
+
import { clientConfig } from "@digimuza/saas-core/config/client";
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
> Orientation and recipes: [`src/config/README.md`](../src/config/README.md).
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
Typed, Zod-validated configuration. Values come from the process environment,
|
|
12
|
+
which **Infisical** populates at runtime — there is no `.env` strategy:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
infisical run -- npm run dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Only two files in the whole package read `process.env`
|
|
19
|
+
(`src/config/env.ts` and `src/config/client.ts`), and a test enforces that.
|
|
20
|
+
Application code must never read it directly.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## `config`
|
|
25
|
+
|
|
26
|
+
An object of lazily-validated sections.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
config.app; // { name, url, environment, logLevel, isProduction }
|
|
30
|
+
config.database; // { url, maxConnections, ssl, statementTimeoutMs, preparedStatements }
|
|
31
|
+
config.auth; // { workosApiKey, workosClientId, cookiePassword, redirectUri }
|
|
32
|
+
config.billing; // { apiKey, webhookSecret, apiUrl }
|
|
33
|
+
config.email; // { apiKey, fromAddress, fromName }
|
|
34
|
+
config.storage; // { bucket, region, accessKeyId, secretAccessKey, endpoint, publicUrl }
|
|
35
|
+
config.realtime; // { url, apiKey }
|
|
36
|
+
config.jobs; // { secretKey, apiUrl }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Why sections validate on first access, not at import
|
|
40
|
+
|
|
41
|
+
A core package is consumed by many runtimes — a migration script, a Trigger.dev
|
|
42
|
+
task, a Next.js route handler — and few need every integration configured.
|
|
43
|
+
Validating everything at import would force every deployment to supply Revolut
|
|
44
|
+
and storage credentials just to run a migration.
|
|
45
|
+
|
|
46
|
+
Each section is validated once, on first read, then memoised.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## `assertConfig(names?)`
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
function assertConfig(names?: readonly SectionName[]): void
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Eagerly validate sections so misconfiguration surfaces at boot rather than
|
|
57
|
+
mid-request. Call it once at startup with what the process actually needs:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
assertConfig(["app", "database", "auth"]);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- Throws `ConfigError` when one section is invalid.
|
|
64
|
+
- Throws `AggregateError` of `ConfigError` when several are, so one boot reports
|
|
65
|
+
every problem instead of making you fix them one at a time.
|
|
66
|
+
- Called with no argument, validates all eight sections.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## `ConfigError`
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
class ConfigError extends Error {
|
|
74
|
+
readonly section: string; // "database"
|
|
75
|
+
readonly variables: readonly string[]; // ["DATABASE_URL"]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The message names the offending **variables** and Zod's structural complaint, and
|
|
80
|
+
**never interpolates values** — so a malformed secret cannot leak into logs,
|
|
81
|
+
stack traces, or an error tracker. There is a test that puts a recognisable
|
|
82
|
+
secret into a failing config and asserts it does not appear in the message.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Environment variables
|
|
87
|
+
|
|
88
|
+
| Section | Variables |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `app` | `APP_NAME`, `APP_URL`, `APP_ENV`¹, `LOG_LEVEL`¹ |
|
|
91
|
+
| `database` | `DATABASE_URL`, `DATABASE_MAX_CONNECTIONS`¹, `DATABASE_SSL`¹, `DATABASE_STATEMENT_TIMEOUT_MS`¹, `DATABASE_PREPARED_STATEMENTS`¹ |
|
|
92
|
+
| `auth` | `WORKOS_API_KEY`, `WORKOS_CLIENT_ID`, `WORKOS_COOKIE_PASSWORD`², `WORKOS_REDIRECT_URI` |
|
|
93
|
+
| `billing` | `REVOLUT_API_KEY`, `REVOLUT_WEBHOOK_SECRET`, `REVOLUT_API_URL`¹ |
|
|
94
|
+
| `email` | `EMAIL_API_KEY`, `EMAIL_FROM_ADDRESS`, `EMAIL_FROM_NAME`³ |
|
|
95
|
+
| `storage` | `STORAGE_BUCKET`, `STORAGE_REGION`, `STORAGE_ACCESS_KEY_ID`, `STORAGE_SECRET_ACCESS_KEY`, `STORAGE_ENDPOINT`³, `STORAGE_PUBLIC_URL`³ |
|
|
96
|
+
| `realtime` | `REALTIME_URL`, `REALTIME_API_KEY` |
|
|
97
|
+
| `jobs` | `TRIGGER_SECRET_KEY`, `TRIGGER_API_URL`¹ |
|
|
98
|
+
|
|
99
|
+
¹ has a default · ² minimum 32 characters · ³ optional
|
|
100
|
+
|
|
101
|
+
**Empty strings count as unset.** Secret managers and CI runners routinely inject
|
|
102
|
+
`FOO=` for absent values; without this, `.default()` and `.optional()` would never
|
|
103
|
+
apply and every deployment would need a placeholder for everything.
|
|
104
|
+
|
|
105
|
+
`DATABASE_PREPARED_STATEMENTS=false` is required behind a transaction-pooling
|
|
106
|
+
proxy (pgBouncer, Supabase or Neon pooler): prepared statements are per-session
|
|
107
|
+
and break when the proxy multiplexes sessions across connections.
|
|
108
|
+
|
|
109
|
+
Variables are named after the credential's **provider** (`REVOLUT_API_KEY`, not
|
|
110
|
+
`BILLING_API_KEY`) so an operator can see what belongs where, and so two
|
|
111
|
+
providers can run side by side during a migration.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Client configuration
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { clientConfig, assertClientConfig } from "@digimuza/saas-core/config/client";
|
|
119
|
+
|
|
120
|
+
clientConfig.app; // { name, url, environment, isProduction }
|
|
121
|
+
clientConfig.auth; // { workosClientId }
|
|
122
|
+
clientConfig.realtime; // { url }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Required: `NEXT_PUBLIC_APP_NAME`, `NEXT_PUBLIC_APP_URL`,
|
|
126
|
+
`NEXT_PUBLIC_WORKOS_CLIENT_ID`. Optional: `NEXT_PUBLIC_APP_ENV` (defaults),
|
|
127
|
+
`NEXT_PUBLIC_REALTIME_URL`.
|
|
128
|
+
|
|
129
|
+
Three things make this safe:
|
|
130
|
+
|
|
131
|
+
1. `config/client` imports **no** server configuration code, so a bundler cannot
|
|
132
|
+
reach a secret through it. Tested.
|
|
133
|
+
2. Every variable is read as a **literal** `process.env.NEXT_PUBLIC_*` access.
|
|
134
|
+
Next.js substitutes those textually at build time; a dynamic
|
|
135
|
+
`process.env[key]` lookup is *not* substituted and would be `undefined` in the
|
|
136
|
+
browser. Do not refactor them into a loop — there is a test that fails if you
|
|
137
|
+
do.
|
|
138
|
+
3. `@digimuza/saas-core/config` and `/db` throw if evaluated in a browser.
|
|
139
|
+
|
|
140
|
+
### Why not the `server-only` package
|
|
141
|
+
|
|
142
|
+
`server-only` resolves to a module that *throws* unless the bundler sets the
|
|
143
|
+
`react-server` export condition. That condition is absent in plain Node, so
|
|
144
|
+
importing it would break every migration script, Trigger.dev task and Vitest run.
|
|
145
|
+
The core uses a runtime browser guard instead. This is weaker than build-time
|
|
146
|
+
enforcement — the structural guarantee is that clients import `/config/client`,
|
|
147
|
+
which never imports server code.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Testing
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
import { setEnvSource, resetConfigCache } from "@digimuza/saas-core/config";
|
|
155
|
+
|
|
156
|
+
resetConfigCache();
|
|
157
|
+
setEnvSource({ DATABASE_URL: "postgres://localhost/test" });
|
|
158
|
+
// ...
|
|
159
|
+
setEnvSource(undefined); // back to process.env
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Vitest merges into the ambient environment rather than replacing it, so tests must
|
|
163
|
+
inject a fixture rather than depend on `process.env`.
|
package/docs/db.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Database & schema
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { getDatabase, createDatabase, runMigrations } from "@digimuza/saas-core/db";
|
|
5
|
+
import { workspaces, users } from "@digimuza/saas-core/db/schema";
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
> Orientation and recipes: [`src/db/README.md`](../src/db/README.md).
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PostgreSQL + Drizzle ORM. Two separate entry points, deliberately:
|
|
12
|
+
|
|
13
|
+
- **`/db/schema`** — table definitions only. No configuration, no driver. Safe for
|
|
14
|
+
drizzle-kit and for your own schema files. A test enforces that it imports
|
|
15
|
+
nothing but `drizzle-orm`.
|
|
16
|
+
- **`/db`** — the client. Server-only, needs configuration.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## `getDatabase()`
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
function getDatabase(): Database
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The shared, configuration-driven handle. Reads the `database` config section on
|
|
27
|
+
first call, so it throws `ConfigError` if the database is not configured.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
const db = getDatabase();
|
|
31
|
+
await db.select().from(users);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The handle is cached on `globalThis`. That is not premature optimisation: Next.js
|
|
35
|
+
dev-mode hot reloading re-evaluates modules on every edit, and without the cache
|
|
36
|
+
each edit would leak a connection pool until PostgreSQL refuses new connections.
|
|
37
|
+
|
|
38
|
+
## `getDatabaseHandle()`
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
function getDatabaseHandle(): DatabaseHandle // { db, client, close() }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
As above, but exposes the raw postgres.js client — for `LISTEN`/`NOTIFY` and raw
|
|
45
|
+
SQL — and the lifecycle.
|
|
46
|
+
|
|
47
|
+
## `createDatabase(options)`
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
function createDatabase(options: {
|
|
51
|
+
url: string;
|
|
52
|
+
maxConnections?: number; // 10
|
|
53
|
+
ssl?: boolean; // false
|
|
54
|
+
statementTimeoutMs?: number; // 30_000
|
|
55
|
+
preparedStatements?: boolean; // true
|
|
56
|
+
}): DatabaseHandle
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
An explicitly configured handle, for when the caller must own the connection
|
|
60
|
+
lifecycle: migrations, tests, one-off scripts.
|
|
61
|
+
|
|
62
|
+
`statement_timeout` is applied per connection so a runaway query cannot hold a
|
|
63
|
+
pool slot indefinitely.
|
|
64
|
+
|
|
65
|
+
## `closeDatabase()`
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
function closeDatabase(): Promise<void>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Close and forget the shared handle. For shutdown hooks and tests.
|
|
72
|
+
|
|
73
|
+
## `CoreDatabase`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import type { CoreDatabase } from "@digimuza/saas-core/db";
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A driver-independent handle typed against the core schema. Every service accepts
|
|
80
|
+
this rather than a concrete `PostgresJsDatabase`, so the same code runs against
|
|
81
|
+
postgres.js in production and PGlite in tests. A `PgTransaction` is assignable to
|
|
82
|
+
it, so any service also composes inside a transaction. There is a compile-time
|
|
83
|
+
assertion proving both drivers unify.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Referencing core tables
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
|
|
91
|
+
import { workspaces } from "@digimuza/saas-core/db/schema";
|
|
92
|
+
|
|
93
|
+
export const projects = pgTable("projects", {
|
|
94
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
95
|
+
workspaceId: uuid("workspace_id")
|
|
96
|
+
.notNull()
|
|
97
|
+
.references(() => workspaces.id, { onDelete: "cascade" }),
|
|
98
|
+
name: text("name").notNull(),
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`drizzle-orm` is a **peer** dependency for exactly this reason: two copies in one
|
|
103
|
+
dependency tree produce table objects Drizzle cannot relate to each other, and the
|
|
104
|
+
failure is a confusing type error far from the cause.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Migrations
|
|
109
|
+
|
|
110
|
+
Versioned migrations ship inside the package (`files: ["drizzle"]`), so an
|
|
111
|
+
application applies core schema changes without vendoring SQL.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { runMigrations, coreMigrationsFolder } from "@digimuza/saas-core/db";
|
|
115
|
+
|
|
116
|
+
await runMigrations(); // core migrations
|
|
117
|
+
await runMigrations({ migrationsFolder: "./drizzle" }); // your own
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm run db:generate # after editing src/db/schema
|
|
122
|
+
infisical run -- npm run db:migrate
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Migrations run on a dedicated single connection: they take advisory locks and must
|
|
126
|
+
not compete with application traffic.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Schema conventions
|
|
131
|
+
|
|
132
|
+
Follow these in your own tables so both halves of the schema stay coherent.
|
|
133
|
+
|
|
134
|
+
- **Explicit `snake_case` names everywhere.** Nothing relies on Drizzle's casing
|
|
135
|
+
inference, so a rename is always a visible diff.
|
|
136
|
+
- **UUID primary keys** from `gen_random_uuid()`. Identifiers appear in URLs;
|
|
137
|
+
sequential ids leak volume and allow enumeration across tenants.
|
|
138
|
+
- **`timestamptz` for all timestamps.** Never naive local time.
|
|
139
|
+
- **`updated_at` is maintained by Drizzle, not a trigger.** Writes that bypass the
|
|
140
|
+
application (manual SQL, data repairs) then show a stale timestamp rather than
|
|
141
|
+
being silently rewritten.
|
|
142
|
+
|
|
143
|
+
Helpers:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { primaryId, timestamps } from "@digimuza/saas-core/db/schema";
|
|
147
|
+
|
|
148
|
+
export const documents = pgTable("documents", {
|
|
149
|
+
id: primaryId(),
|
|
150
|
+
...timestamps, // createdAt, updatedAt
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Constraint error introspection
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { isUniqueViolation, isForeignKeyViolation, getPostgresError } from "@digimuza/saas-core/db";
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Tenant isolation in this schema *is* a set of database constraints, so services
|
|
163
|
+
need to tell "duplicate slug" from "cross-workspace role" from an unrelated
|
|
164
|
+
failure.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
try {
|
|
168
|
+
await db.insert(workspaces).values({ name, slug });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (isUniqueViolation(error, "workspaces_slug_unique")) {
|
|
171
|
+
throw new ConflictError("Slug taken.");
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Always pass the constraint name. Matching on the code alone will absorb unrelated
|
|
178
|
+
violations — a bug the core itself had: `ensureCustomer` treated *any* unique
|
|
179
|
+
violation as "a concurrent request won the race", which silently swallowed a
|
|
180
|
+
provider returning an id already bound to another workspace.
|
|
181
|
+
|
|
182
|
+
The helper walks the error's `cause` chain and accepts both spellings, because
|
|
183
|
+
postgres.js puts `code`/`constraint_name` on the thrown error while Drizzle's
|
|
184
|
+
PGlite driver nests `code`/`constraint` under `cause`.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Table ownership
|
|
189
|
+
|
|
190
|
+
Core-owned: `users`, `user_profiles`, `workspaces`, `workspace_members`,
|
|
191
|
+
`workspace_invitations`, `roles`, `permissions`, `role_permissions`,
|
|
192
|
+
`workspace_member_roles`, `plans`, `entitlements`, `billing_customers`,
|
|
193
|
+
`subscriptions`, `billing_events`, `usage_records`, `events`.
|
|
194
|
+
|
|
195
|
+
Your application owns everything else. Do not add product-specific models to the
|
|
196
|
+
core unless they clearly apply to several products.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Testing against real PostgreSQL
|
|
201
|
+
|
|
202
|
+
The core's own tests use `@electric-sql/pglite` — PostgreSQL compiled to WASM — and
|
|
203
|
+
run the real migrations:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
207
|
+
import { drizzle } from "drizzle-orm/pglite";
|
|
208
|
+
import { migrate } from "drizzle-orm/pglite/migrator";
|
|
209
|
+
import { coreMigrationsFolder } from "@digimuza/saas-core/db";
|
|
210
|
+
import * as schema from "@digimuza/saas-core/db/schema";
|
|
211
|
+
|
|
212
|
+
const client = new PGlite();
|
|
213
|
+
const db = drizzle(client, { schema });
|
|
214
|
+
await migrate(db, { migrationsFolder: coreMigrationsFolder });
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Real PostgreSQL rather than a mock, because the guarantees under test *are*
|
|
218
|
+
database constraints — composite foreign keys, partial unique indexes, cascades. A
|
|
219
|
+
fake would happily accept the cross-tenant writes the schema exists to reject.
|