.github/9
ISSUE_TEMPLATE/3bug_report.md config.yml feature_request.md
workflows/3ci.yml clean-clone.yml release.yml
CODEOWNERS dependabot.yml pull_request_template.md
apps/630
api/250
src/243
common/30
__tests__/3authorize.test.ts bigint-serialization.interceptor.test.ts files-limits.test.ts
filters/1all-exceptions.filter.ts
guards/3auth.guard.ts internal-service.guard.ts rate-limit.guard.ts
interceptors/2actor-context.interceptor.ts bigint-serialization.interceptor.ts
middleware/2idempotency.test.ts idempotency.ts
pipes/1zod-validation.pipe.ts
actor-context.ts audit.service.ts cache.test.ts cache.ts cookies.ts core.module.ts deprecation.ts email.service.ts errors.ts event-bus.ts log-scrubber.test.ts log-scrubber.ts logger.ts permission-cache-bridge.ts prisma.service.ts redis.service.ts trace-metadata.ts types.ts
config/1env.ts
lib/11
__tests__/5crypto.test.ts filter-builder.test.ts jwt.test.ts pagination.test.ts positions.test.ts
authorize.ts crypto.ts filter-builder.ts jwt.ts pagination.ts positions.ts
modules/197
ai/28
__tests__/4ai-writing.test.ts database-ai.test.ts generate-executor.test.ts prompts.test.ts
generator/4executor.ts generate.controller.ts generate.dto.ts generate.service.ts
ai-chat.service.ts ai-rate-limit.ts ai-writing.controller.ts ai-writing.dto.ts ai-writing.service.ts ai.controller.ts ai.module.ts ai.service.ts budget-guard.service.ts database-ai.controller.ts database-ai.dto.ts database-ai.service.ts db-prompts.ts dto.ts gateway-provider.ts ledger.service.ts prompts.ts rag.controller.ts rag.dto.ts rag.service.ts
api-keys/3api-keys.controller.ts api-keys.module.ts api-keys.service.ts
audit/2audit-log.controller.ts audit.module.ts
auth/4auth.controller.ts auth.module.ts auth.service.ts dto.ts
automations/5automations.controller.ts automations.module.ts automations.service.ts dto.ts webhook-incoming.controller.ts
blocks/5
__tests__/1batch-version-cas.test.ts
block.service.ts blocks.controller.ts blocks.module.ts dto.ts
comments/4comments.controller.ts comments.module.ts comments.service.ts dto.ts
databases/9
__tests__/3normalize-select-options.test.ts option-id-canonicalization.test.ts record-open-query-shape.test.ts
database.service.ts databases.controller.ts databases.module.ts dto.ts property-cache-schema.ts query.service.ts
docs/2docs.controller.ts docs.module.ts
files/10
__tests__/1quota.test.ts
dto.ts files.controller.ts files.module.ts files.service.ts quota.ts record.ts s3.ts uploads.controller.ts uploads.service.ts
forms/18debug.controller.ts form.dto.ts forms.controller.ts forms.module.ts forms.service.ts markdown.test.ts markdown.ts optional-auth.guard.ts public-forms.controller.ts public-forms.service.ts question.dto.ts questions.service.ts rate-limit.ts recaptcha.ts slug.ts submissions.controller.ts submissions.service.ts submit-schema.ts
health/3health.controller.ts health.module.ts health.service.ts
integrations/3integrations.controller.ts integrations.module.ts integrations.service.ts
internal/12
__tests__/2flush-query-shape.test.ts internal.endpoints.test.ts
ai-test-seam.controller.ts ai-test-seam.dto.ts ai-test-seam.service.ts internal.controller.ts internal.dto.ts internal.module.ts internal.service.ts test-seam.controller.ts test-seam.dto.ts test-seam.service.ts
notifications/3notifications.controller.ts notifications.module.ts notifications.service.ts
oauth/6
__tests__/1oauth.scopes.test.ts
oauth.controller.ts oauth.module.ts oauth.service.ts scopes.ts token-keys.ts
org/15audit.service.ts embed-preview.controller.ts guards.ts members.service.ts org-admin.controller.ts org-policy.service.ts org.module.ts overview.service.ts policy-effective.controller.ts policy.schema.ts policy.test.ts retention.service.ts totp.test.ts totp.ts two-factor.service.ts
orgs/5dto.ts orgs.controller.ts orgs.module.ts orgs.service.ts util.ts
pages/4dto.ts page.service.ts pages.controller.ts pages.module.ts
scim/15
__tests__/3email-mapping.test.ts etag.test.ts filters.test.ts
etag.ts filters.ts groups.service.ts schemas.ts scim-admin.controller.ts scim-admin.service.ts scim-auth.guard.ts scim-auth.service.ts scim-errors.ts scim.controller.ts scim.module.ts users.service.ts
search/4
__tests__/1search.service.test.ts
search.controller.ts search.module.ts search.service.ts
sessions/3sessions.controller.ts sessions.module.ts sessions.service.ts
share/3share.controller.ts share.module.ts share.service.ts
sso/16
__tests__/4enforcement.test.ts oidc.verify.test.ts provisioning.test.ts saml.validate.test.ts
enforcement.ts oidc.controller.ts oidc.service.ts oidc.ts provisioning.service.ts saml.controller.ts saml.ts schemas.ts sso-admin.controller.ts sso-admin.service.ts sso-secrets.ts sso.module.ts
teams/4dto.ts teams.controller.ts teams.module.ts teams.service.ts
users/4dto.ts users.controller.ts users.module.ts users.service.ts
webhooks/3webhooks.controller.ts webhooks.module.ts webhooks.service.ts
workspaces/4dto.ts workspaces.controller.ts workspaces.module.ts workspaces.service.ts
types/1xmldom.d.ts
app.module.ts constants.ts main.ts
_probe_rawbody.cjs .env.example Dockerfile package.json README.md tsconfig.json vitest.config.ts
realtime/33
src/17auth.ts comments.ts config.ts event-source.ts http-store.ts index.ts metrics.ts notifications.ts page-updated-source.ts presence.ts prisma-store.ts protocol.ts rate-limit.ts room-key.ts rooms.ts server.ts update-log.ts
test/11auth.test.ts config.test.ts event-source.test.ts http-store.test.ts metrics-merge.test.ts page-updated-source.test.ts presence.test.ts protocol.test.ts rate-limit.test.ts rooms.test.ts task-0094-builders.test.ts
.env.example Dockerfile package.json README.md tsconfig.json
web/255
src/244
app/45
__tests__/1theme-tokens.test.ts
(app)/26
w/25
[workspace]/25
ask/1page.tsx
databases/1
[id]/1page.tsx
favorites/1page.tsx
files/1page.tsx
notifications/1page.tsx
page/4
[id]/4
[block]/1page.tsx
share/1page.tsx
layout.tsx page.tsx
search/1page.tsx
settings/10
appearance/1page.tsx
automations/3
[id]/1page.tsx
new/1page.tsx
page.tsx
billing/1page.tsx
developers/1page.tsx
members/1page.tsx
permissions/1page.tsx
teams/1page.tsx
page.tsx
shared/1page.tsx
templates/1page.tsx
trash/1page.tsx
layout.tsx page.tsx
layout.tsx
(auth)/4
login/1page.tsx
register/1page.tsx
reset-password/1page.tsx
layout.tsx
api/2
metrics/1route.ts
v1/1
health/1route.ts
f/1
[slug]/1page.tsx
health/1
ready/1route.ts
org/2
[orgId]/2
admin/2
integrations/1page.tsx
page.tsx
share/1
[id]/1page.tsx
error.tsx global-error.tsx globals.css layout.tsx not-found.tsx page.tsx providers.tsx
components/118
admin/10
org-admin/8ApiKeysTab.tsx AuditTab.tsx MembersTab.tsx OverviewTab.tsx PoliciesTab.tsx RetentionTab.tsx WebhooksTab.tsx WorkspacePicker.tsx
ScimCard.tsx SsoTabs.tsx
ai/6ai-panel.tsx ask-ai-submenu.tsx db-ai-dialog.tsx generate-page-dialog.tsx writing-actions-menu.tsx writing-diff.tsx
automations/5action-editor.tsx condition-editor.tsx lib.ts run-history.tsx trigger-editor.tsx
collaboration/6CursorLayer.tsx OfflineBanner.tsx PresenceBar.tsx README.md ReviewFailedDialog.tsx StatusChip.tsx
comments/3comment-panel.tsx comment-thread.tsx mention-input.tsx
database/27
__tests__/2cell-editor.test.tsx cell-renderer.test.tsx
AiExtractDialog.tsx AiFormulaDialog.tsx AiGenerateDialog.tsx board-view.tsx buttons-panel.tsx calendar-view.tsx cell-editor.tsx cell-renderer.tsx chart-view.tsx database-page-view.tsx database-toolbar.tsx database-view-renderer.tsx db-embedded.tsx filter-builder.tsx form-builder-view.tsx formula-editor.tsx gallery-view.tsx group-builder.tsx list-view.tsx property-editor.tsx record-header.tsx record-panel.tsx sort-builder.tsx table-view.tsx timeline-view.tsx
dialogs/7
__tests__/1move-to-dialog.test.tsx
create-dialog.tsx dialog.tsx file-permissions-dialog.tsx invite-dialog.tsx move-to-dialog.tsx share-dialog.tsx
editor/19
__tests__/3code-block-text.test.ts code-block.test.tsx expandable.test.tsx
block-card.tsx block-renderer.tsx block-save-queue.ts block-toolbar.tsx context-menu.tsx expandable.tsx history-popover.tsx inline-editor.tsx insert-dialog.tsx page-canvas.tsx page-spinner.tsx page-view.tsx selection-toolbar.tsx slash-menu.tsx template-dialog.tsx type-menu.tsx
files/4FileBlock.tsx FileBrowser.tsx FilePicker.tsx PreviewModal.tsx
form/4form-field.tsx form-renderer.tsx form-responses-view.tsx form-view.tsx
layout/8
__tests__/1page-tree-icon.test.tsx
command-palette.tsx factory-logo.tsx page-tree-node.tsx sidebar.tsx theme-provider.tsx topbar.tsx workspace-shell.tsx
mentions/3
__tests__/1mention-model.test.ts
mention-model.ts MentionPopup.tsx
search/3ask-ai-search.tsx results-list.tsx search-dialog.tsx
settings/10
api-keys/4ApiKeyCreateDialog.tsx ApiKeyRevealDialog.tsx ApiKeyTable.tsx ScopesBadge.tsx
webhooks/5CurlExamples.tsx DeliveryInspector.tsx EventExplorer.tsx OverviewTab.tsx WebhooksTab.tsx
settings-shell.tsx
ui/3confirm-dialog.tsx form.tsx toaster.tsx
hooks/21
__tests__/4realtime-frame-handler.test.ts use-blocks.test.ts use-database-options.test.ts use-workspace.test.tsx
realtime-frame-handler.ts use-ai-stream.ts use-auth.ts use-blocks.ts use-database.ts use-db-ai-stream.ts use-forms.ts use-members.ts use-mention-sources.ts use-notifications.ts use-offline-status.ts use-pages.ts use-presence-announcer.ts use-presence-toasts.ts use-presence.ts use-realtime.ts use-workspace.ts
lib/50
__tests__/21ai-database.test.ts ai-generate.test.ts ai-sse.test.ts ai-writing.test.ts api-keys.test.ts api-members.test.ts auth-refresh.test.ts auth-store.test.ts batch-queue-drop.test.ts block-content.test.ts clipboard-import.test.ts code-highlight.test.ts offline-status.test.ts presence-announcer.test.ts presence-color.test.ts presence-cursor.test.ts presence-emitter.test.ts realtime-url.test.ts search.test.ts utils.test.ts writing-diff.test.ts
files/7
__tests__/2picker.test.ts upload.test.ts
api.ts picker.ts upload-store.ts upload.ts use-file-uploads.ts
ai-database.ts ai.ts api-keys.ts api.ts auth-store.ts batch-queue.ts block-content.ts clipboard-import.ts code-highlight.ts constants.ts offline-status.ts org-admin.ts presence-announcer.ts presence-color.ts presence-cursor.ts presence-editor-views.ts presence-emitter.ts query-client.ts realtime.ts search.ts types.ts utils.ts
stores/8
__tests__/1block-patch-store.test.ts
block-patch-store.ts comment-pill-store.ts database-store.ts editor-store.ts offlineQueue.ts presence-store.ts ui-store.ts
instrumentation.ts middleware.ts
.env.example Dockerfile next-env.d.ts next.config.mjs package.json postcss.config.mjs README.md tailwind.config.ts tsconfig.json tsconfig.tsbuildinfo vitest.config.ts
worker/92
src/87
__tests__/13ai-embedding.test.ts ai-request.test.ts ai-usage-budget.test.ts audit-export.test.ts email-decision.test.ts encrypt.test.ts health-search-index-lag.test.ts mime.test.ts retention.test.ts s3-sign.test.ts search-index.test.ts smtp.test.ts webhook-sign.test.ts
health/1server.ts
lib/12
__tests__/1clamav.test.ts
clamav.ts encrypt.ts job-otel.ts mime.ts notify.ts prisma.ts redis-stream.ts redis.ts s3.ts sharp.ts smtp.ts
processors/56
automations/28
__tests__/3condition-evaluator.spec.ts executors.spec.ts run-pipeline.spec.ts
executors/17assign-user.ts call-integration.ts change-status.ts classify.ts create-notification.ts create-page.ts email-cap.ts helpers.ts integration-registry.ts permission.ts record-write.ts registry.ts run-ai-action.ts send-email.ts send-webhook.ts types.ts update-record.ts
condition-evaluator.ts dead-letter.ts log.ts matcher.ts pipeline.ts run-state.ts schedule.ts watchdog.ts
files/15
__tests__/4errors.test.ts scan-gate.test.ts status.test.ts temp.test.ts
steps/4finalize.ts metadata.ts scan.ts thumbnails.ts
cleanup.ts errors.ts generateThumbs.ts processor.ts status.ts temp.ts types.ts
search-index/4builders.ts grants.ts index.ts processor.ts
ai-embedding.ts ai.processor.ts audit-export.ts automations.processor.ts email-decision.processor.ts email.processor.ts maintenance.processor.ts retention.ts webhooks.processor.ts
config.ts events.ts index.ts logger.ts queues.ts
.env.example Dockerfile package.json README.md tsconfig.json
docs/73
api/17ai.md authentication.md automations.md blocks.md comments.md databases.md files.md forms.md openapi.yaml org-admin.md pages.md scim.md search.md sso.md versioning.md webhooks.md workspaces.md
architecture/10ai-architecture.md automation.md backend-architecture.md database-architecture.md frontend-architecture.md realtime-architecture.md search-architecture.md security-architecture.md storage.md system-architecture.md
database/2database-architecture.md redis-keys.md
development/8admin-guide.md coding-standards.md deployment.md development-guide.md files-pipeline.md perf-report.md performance.md testing.md
product/4functional-requirements.md non-functional-requirements.md product-requirements.md ux-requirements.md
requirements/1requirements-matrix.md
security/5audit-report.md files.md forms.md license-allowlist.md security-architecture.md
tasks/16phase-00-foundation.md phase-01-authentication.md phase-02-workspace.md phase-03-pages.md phase-04-editor.md phase-05-databases.md phase-06-relations.md phase-07-collaboration.md phase-08-search.md phase-09-files.md phase-10-forms.md phase-11-automation.md phase-12-ai.md phase-13-api.md phase-14-enterprise.md phase-15-production.md
ui/9ai.md automations.md design-system.md developers.md files.md forms.md org-admin.md scim.md sso.md
support-matrix.md
infrastructure/71
caddy/1Caddyfile
docker/6
init/101-extensions.sql
opensearch/1init-indices.json
.env.docker docker-compose.yml opensearch-init.sh README.md
kubernetes/40
observability/20
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1sync-sources.mjs
alertmanager.yaml grafana.yaml kustomization.yaml namespace.yaml node-exporter.yaml otel-collector.yaml prometheus.yaml
postgres/7configmap.yaml cronjob-backup.yaml cronjob-opensearch-snapshot.yaml pgbouncer.yaml replica.yaml statefulset.yaml walg-iam-policy.yaml
api.yaml configmap.yaml ingress.yaml kustomization.yaml namespace.yaml networkpolicy.yaml pdb.yaml README.md realtime.yaml redis.yaml secret.example.yaml web.yaml worker.yaml
nginx/2nginx.conf README.md
observability/14
alertmanager/1alertmanager.yaml
grafana/8
dashboards/6ai.json api-overview.json infra.json realtime.json search.json workers-queues.json
provisioning/2
dashboards/1dashboards.yaml
datasources/1datasource.yaml
prometheus/3alerts.yaml recording-rules.yaml scrape.yaml
scripts/1dashboard-completeness.mjs
README.md
terraform/8compute.tf data.tf main.tf network.tf outputs.tf README.md security.tf variables.tf
packages/289
ai/24
src/20
providers/7anthropic.ts google.ts http.ts index.ts openai-compatible.ts rag.test.ts writing.test.ts
schemas/2database.ts generate.ts
adapters.test.ts config.test.ts config.ts gateway.test.ts gateway.ts index.ts tools.test.ts tools.ts types.ts usage.test.ts usage.ts
package.json README.md tsconfig.json vitest.config.ts
auth/11
src/7index.ts passwords.test.ts passwords.ts sessions.test.ts sessions.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
collaboration/17
src/13awareness.ts codec.test.ts codec.ts index.ts lww.test.ts lww.ts mapping.ts offline.ts outbox.test.ts outbox.ts provider.test.ts provider.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
config/7
src/4eslint.base.js index.ts prettier.base.json tsconfig.base.json
package.json README.md tsconfig.json
database-client/5
src/2index.test.ts index.ts
package.json README.md tsconfig.json
database-engine/33
src/29
filter/2filter.test.ts index.ts
formula/8evaluator.ts formula.test.ts functions.ts index.ts lexer.ts parser.ts types.ts validate.ts
query/6group.ts index.ts paginate.ts pivot.ts query.test.ts sort.ts
relations/2index.ts relations.test.ts
rollups/2index.ts rollups.test.ts
values/5dates.ts index.ts registry.ts types.ts values.test.ts
views/3buttons.ts index.ts views.test.ts
index.ts
package.json README.md tsconfig.json vitest.config.ts
editor/25
src/21
marks/1highlight.ts
nodes/2equation.ts mention.ts
autosave.test.ts autosave.ts clipboard.test.ts clipboard.ts codec.test.ts codec.ts context-menu.test.ts context-menu.ts diff.test.ts diff.ts factory.ts history.test.ts history.ts index.ts positions.ts slash.test.ts slash.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
observability/20
src/16
__tests__/8db-redis-metrics.test.ts metrics.test.ts otel.test.ts pino-otel.test.ts prometheus.test.ts resource.test.ts scrubber.test.ts traceparent.test.ts
index.ts metrics.ts otel.ts pino-otel.ts prometheus.ts resource.ts scrubber.ts traceparent.ts
package.json README.md tsconfig.json vitest.config.ts
permissions/11
src/7index.ts levels.test.ts levels.ts resolve.test.ts resolve.ts rules.test.ts rules.ts
package.json README.md tsconfig.json vitest.config.ts
search/22
src/18chunking.test.ts chunking.ts client.test.ts client.ts embeddings.test.ts embeddings.ts hybrid.test.ts hybrid.ts index.ts indexer.test.ts indexer.ts mapping.test.ts mapping.ts queries.test.ts queries.ts rrf.test.ts rrf.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
shared/61
src/57
dto/17ai.dto.ts auth.dto.test.ts auth.dto.ts block.dto.ts browser-safety.test.ts comment.dto.ts database.dto.test.ts database.dto.ts file.dto.ts formula-ast.ts index.ts notification.dto.ts page.dto.ts search.dto.ts share.dto.ts webhook.dto.ts workspace.dto.ts
events/8audit-catalog.test.ts audit-catalog.ts audit-csv.test.ts audit-csv.ts audit-scope.test.ts audit-scope.ts catalog.test.ts catalog.ts
realtime/6comment-events.ts frames.test.ts frames.ts notification-policy.ts notification-prefs.ts task-0094.test.ts
automation.test.ts automation.ts constants.ts crypto.test.ts crypto.ts env.ts errors.ts events.test.ts events.ts forms.test.ts forms.ts index.ts markdown.test.ts markdown.ts mentions.test.ts mentions.ts net.test.ts net.ts positions.test.ts positions.ts presence.test.ts presence.ts security-headers.ts webhook-delivery.test.ts webhook-delivery.ts webhook-events.ts
package.json README.md tsconfig.json vitest.config.ts
storage/16
src/12client.test.ts client.ts index.ts keys.test.ts keys.ts mime.test.ts mime.ts presign.test.ts presign.ts thumbnails.test.ts thumbnails.ts types.ts
package.json README.md tsconfig.json vitest.config.ts
ui/37
src/33
components/19avatar.tsx badge.tsx button.tsx checkbox.tsx command.tsx context-menu.tsx dialog.tsx dropdown-menu.tsx empty-state.tsx index.ts popover.tsx separator.tsx skeleton.tsx slider.tsx spinner.tsx switch.tsx tabs.tsx toast.tsx tooltip.tsx
color.ts command-filter.test.ts command-filter.ts focus.test.ts focus.ts formatters.test.ts formatters.ts index.ts theme.test.ts theme.ts toast-store.test.ts toast-store.ts tokens.test.ts tokens.ts
package.json README.md tsconfig.json vitest.config.ts
prisma/16
migrations/13
20260927000000_init/1migration.sql
20260928000000_search_documents_content_hash/1migration.sql
20260929000000_workspace_usage_storage_bytes/1migration.sql
20260930000000_form_submitted_notification_type/1migration.sql
20261001000000_ai_phase12/1migration.sql
20261002000000_webhooks_secret_encrypted_dlq/1migration.sql
20261002111436_webhook_delivery_latency/1migration.sql
20261002120000_org_admin_2fa_retention_unique/1migration.sql
20261002215124_audit_export/1migration.sql
20261003210000_perf_indexes_hot_paths/1migration.sql
20261004004500_perf_indexes_hot_paths_2/1migration.sql
migration_lock.toml README.md
schema.prisma seed.ts verify.ts
scripts/7backup-health.ts docs-completeness.mjs generate-openapi.mjs minio-init.ts opensearch-reindex.ts storage-orphans.ts verify-clean-clone.sh
tests/169
e2e/63
.auth/1user.json
fixtures/7
files/6clip.mp4 clip.wav clip.webm hero-1400.png note.docx note.txt
realtime.ts
mocks/2oidc-issuer.ts saml-idp.ts
playwright-report/3
data/2a6557bae9c2714a658968d4eb8534195c08ebda8.png e3092d07d00a16063e46385c02a559a9054fa847.webm
index.html
reports/1files-security.json
specs/34ai-database.spec.ts ai-generate.spec.ts ai-rag.spec.ts ai-security.spec.ts ai-writing.spec.ts ai.spec.ts api-keys.spec.ts appearance-theme.spec.ts auth.spec.ts automation.spec.ts collab-two-tab.spec.ts comments.spec.ts databases.spec.ts default-workspace-alias.spec.ts developers.spec.ts editor.spec.ts files.spec.ts final-flow.spec.ts forms.spec.ts oauth.spec.ts org-admin.spec.ts pages.spec.ts performance.spec.ts presence-cursors.spec.ts qa-paste-toggle.spec.ts realtime.spec.ts scim.spec.ts search-index-lag.spec.ts search.spec.ts sharing.spec.ts sso.spec.ts task-0094-comments-notifications.spec.ts webhooks.spec.ts workspace.spec.ts
test-results-final-flow/3
final-flow-Release-v1-0-fi-6c06b-w-state-carries-sequential--chromium-desktop/2test-failed-1.png trace.zip
.last-run.json
utils/4api.ts dnd.ts search-lag.ts wait.ts
.gitignore fixtures.ts global-setup.ts package.json playwright.config.ts playwright.final-flow.config.ts README.md tsconfig.json
fixtures/1
files/1make-sparse.ts
integration/60
ai-capability/2ai-app.capability.test.ts gateway.capability.test.ts
drill/2seed-drill.mjs verify-restore.mjs
fixtures/5
files/5clip.mp4 clip.wav doc.pdf hero-1400.png note.txt
helpers/3api.ts containers.ts seed.ts
realtime/11control-channel.test.ts convergence.test.ts flush-rebase-http.test.ts handshake.test.ts heartbeat-limits.test.ts lifecycle.test.ts presence.test.ts rebase.test.ts resume.test.ts rt-helpers.ts task-0094-fanout.test.ts
ai-chat.integration.test.ts ai-database.integration.test.ts ai-generate.integration.test.ts ai-rag.integration.test.ts ai-writing.integration.test.ts api-keys.integration.test.ts audit-completeness.integration.test.ts audit-export-s3.integration.test.ts auth.integration.test.ts automation-pipeline.integration.test.ts automations.integration.test.ts blocks.integration.test.ts cache-invalidation.integration.test.ts comments.integration.test.ts databases.integration.test.ts files-lifecycle.integration.test.ts files-pipeline.integration.test.ts files-security.integration.test.ts files.integration.test.ts forms-responses.integration.test.ts forms.integration.test.ts oauth.integration.test.ts openapi.contract.test.ts org-admin.integration.test.ts package.json pages.integration.test.ts permissions.integration.test.ts README.md replica-routing.integration.test.ts retention-engine.integration.test.ts scim.integration.test.ts search.integration.test.ts sso.integration.test.ts tsconfig.json vitest.config.ts webhooks.integration.test.ts workspace.integration.test.ts
performance/28
fixtures/1search-100k.ts
helpers/2seed-100k.mjs seed-1k-load.mjs
reports/7api-load-smoke.json database-load-smoke.json editor-local.json realtime-load.json search-full.json search-last.json search-reduced.json
api-load.js api.js archive.tar database-load.js database.js editor-local.mjs extract-100k-env.mjs package.json README.md realtime-1k.js realtime-load.js realtime.js run-realtime-load.sh search-search.k6.js search-summary.json seed-100k.mjs threshold-breach-demo.js verify-zero-op-loss.mjs
security/16
helpers/1app.ts
audit-report.test.ts auth-paths.test.ts deps.test.ts formula-fuzz.test.ts gitleaks-config.test.ts headers.test.ts images.test.ts oracle.ts package.json permission-fuzz.test.ts rate-limits.test.ts README.md tsconfig.json upload-abuse.test.ts vitest.config.ts
README.md
.dockerignore .env .env.bak-task0125 .env.example .eslintrc.json .gitignore .npmrc .prettierignore .prettierrc.json AGENTS.md ARCHITECTURE.md CHANGELOG.md CONTRIBUTING.md gitleaks.toml LICENSE package.json pnpm-lock.yaml pnpm-workspace.yaml PRODUCT-SPEC.md README.md ROADMAP.md SECURITY.md tsconfig.json turbo.json
schema.prismaprisma/schema.prismaraw← prisma57,424 B← Back to the app
1// ---------------------------------------------------------------------------
2// AI Software Factory — Prisma schema (PostgreSQL)
3//
4// Conventions:
5//  - Every tenant-scoped table carries workspaceId (and organizationId where
6//    relevant). All queries are tenant-scoped in the repository layer.
7//  - Ordering uses fractional positions (LexoRank-style strings), NEVER indexes.
8//  - Flexible payloads (block content, property values, view settings,
9//    formula ASTs) are JSONB.
10//  - Records are pages: database_records.pageId is 1:1 with pages.id.
11//  - Polymorphic references (SearchDocument.refId, Permission.resourceId,
12//    Share.recordId, Comment.pageMentionIds) are plain strings — the
13//    application validates referential integrity, Prisma does not model
14//    multi-way FKs on shared columns.
15// ---------------------------------------------------------------------------
16 
17generator client {
18  provider = "prisma-client-js"
19  // TASK-0135 (observability): the `tracing` preview feature (the generated
20  // client's real tracing helper, delegating to
21  // globalThis.PRISMA_INSTRUMENTATION at query time) was promoted to GA in
22  // Prisma 6.1 — no previewFeatures entry is needed for @prisma/instrumentation
23  // spans (db_query_duration_seconds) to be emitted (T-190b).
24}
25 
26datasource db {
27  provider = "postgresql"
28  url      = env("DATABASE_URL")
29}
30 
31// ============ ENUMS ============
32 
33enum WorkspaceRole {
34  OWNER
35  ADMIN
36  MEMBER
37  GUEST
38}
39 
40enum PermissionLevel {
41  NONE
42  VIEW
43  COMMENT
44  EDIT
45  FULL_ACCESS
46}
47 
48enum ShareScope {
49  PRIVATE
50  WORKSPACE
51  TEAM
52  USERS
53  PUBLIC
54}
55 
56enum BlockType {
57  PARAGRAPH
58  HEADING_1
59  HEADING_2
60  HEADING_3
61  QUOTE
62  CALLOUT
63  BULLETED_LIST
64  NUMBERED_LIST
65  TODO
66  TOGGLE
67  IMAGE
68  VIDEO
69  AUDIO
70  FILE
71  BOOKMARK
72  EMBED
73  CODE
74  EQUATION
75  COLUMN
76  COLUMN_LIST
77  DIVIDER
78  SPACER
79  DATABASE
80  LINKED_DATABASE
81  TABLE
82  CHART
83  FORM
84  SYNCED_BLOCK
85  CHILD_PAGE
86  CHILD_DATABASE
87  // T-147: table-of-contents block (content generated from headings at render).
88  TOC
89}
90 
91enum PropertyType {
92  TITLE
93  TEXT
94  RICH_TEXT
95  NUMBER
96  SELECT
97  MULTI_SELECT
98  STATUS
99  DATE
100  DATE_RANGE
101  PERSON
102  EMAIL
103  PHONE
104  URL
105  CHECKBOX
106  FILES
107  CREATED_TIME
108  CREATED_BY
109  LAST_EDITED_TIME
110  LAST_EDITED_BY
111  RELATION
112  ROLLUP
113  FORMULA
114  BUTTON
115  UNIQUE_ID
116}
117 
118enum ViewType {
119  TABLE
120  LIST
121  BOARD
122  GALLERY
123  CALENDAR
124  TIMELINE
125  CHART
126  DASHBOARD
127  FORM
128}
129 
130enum RelationCardinality {
131  ONE_TO_ONE
132  ONE_TO_MANY
133  MANY_TO_MANY
134}
135 
136enum RollupAggregate {
137  COUNT
138  SUM
139  AVERAGE
140  MIN
141  MAX
142  PERCENT
143  UNIQUE
144}
145 
146enum AutomationTriggerType {
147  RECORD_CREATED
148  RECORD_UPDATED
149  PROPERTY_CHANGED
150  PAGE_CREATED
151  FORM_SUBMITTED
152  SCHEDULE
153  WEBHOOK
154  MANUAL
155}
156 
157enum AutomationActionType {
158  UPDATE_RECORD
159  CREATE_PAGE
160  SEND_EMAIL
161  SEND_WEBHOOK
162  CREATE_NOTIFICATION
163  ASSIGN_USER
164  CHANGE_STATUS
165  CALL_INTEGRATION
166  RUN_AI_ACTION
167}
168 
169enum AutomationStatus {
170  DRAFT
171  ACTIVE
172  PAUSED
173  ARCHIVED
174}
175 
176enum AutomationRunStatus {
177  PENDING
178  RUNNING
179  SUCCEEDED
180  FAILED
181  DEAD_LETTER
182}
183 
184enum NotificationChannel {
185  IN_APP
186  EMAIL
187  WEBHOOK
188}
189 
190enum NotificationType {
191  MENTION
192  FORM_SUBMITTED
193  COMMENT
194  ASSIGNMENT
195  PAGE_SHARED
196  TASK_ASSIGNED
197  DATABASE_CHANGE
198  AUTOMATION
199  INTEGRATION
200  // Confluence expansion (T-163/T-157/T-160):
201  REVIEW_REQUESTED   // page submitted for review → reviewers
202  APPROVAL_DECISION  // approved / changes requested → editor + other reviewers
203  WATCH_UPDATE       // watched page changed → watchers (canView rechecked at fan-out)
204  TASK_DUE           // task due-date reminder → assignees
205}
206 
207enum FileStatus {
208  UPLOADING
209  SCANNING
210  READY
211  VIRUS_DETECTED
212  FAILED
213}
214 
215enum FormQuestionType {
216  SHORT_TEXT
217  LONG_TEXT
218  NUMBER
219  EMAIL
220  DATE
221  CHECKBOX
222  SINGLE_SELECT
223  MULTI_SELECT
224  RATING
225  FILE
226}
227 
228enum FormStatus {
229  DRAFT
230  PUBLISHED
231  CLOSED
232}
233 
234enum AuditActorType {
235  USER
236  API_KEY
237  SYSTEM
238  INTEGRATION
239}
240 
241enum ApiKeyScope {
242  READ
243  WRITE
244  ADMIN
245}
246 
247enum WebhookDeliveryStatus {
248  PENDING
249  DELIVERED
250  FAILED
251  REJECTED
252  /// TASK-0130 — terminal after the final retry (8 attempts) fails: the
253  /// delivery is dead-lettered (kept in `webhook_deliveries` for the
254  /// inspector; resubmit re-queues it).
255  DEAD_LETTER
256}
257 
258enum UserStatus {
259  ACTIVE
260  INVITED
261  SUSPENDED
262}
263 
264enum InvitationStatus {
265  PENDING
266  ACCEPTED
267  DECLINED
268  REVOKED
269  EXPIRED
270}
271 
272enum SessionStatus {
273  ACTIVE
274  REVOKED
275  EXPIRED
276}
277 
278enum EmailVerificationStatus {
279  UNVERIFIED
280  PENDING
281  VERIFIED
282}
283 
284enum PasswordResetStatus {
285  PENDING
286  USED
287  EXPIRED
288  REVOKED
289}
290 
291enum SSOProviderType {
292  SAML
293  OIDC
294}
295 
296enum RetentionPolicyType {
297  PURGE
298  HOLD
299}
300 
301// ============ IDENTITY & TENANCY ============
302 
303model User {
304  id                 String        @id @default(cuid())
305  email              String        @unique
306  emailVerifiedAt    DateTime?
307  emailVerification  EmailVerificationStatus @default(UNVERIFIED)
308  passwordHash       String?
309  name               String
310  displayName        String?
311  avatarUrl          String?
312  status             UserStatus    @default(ACTIVE)
313  locale             String        @default("en-US")
314  timezone           String        @default("UTC")
315  theme              String        @default("system")
316  // Per-type EMAIL-channel notification preferences (TASK-0094 R6): a
317  // partial map NotificationType -> "all"|"instant"|"digest"|"off"
318  // (IN_APP is always on). The shape is the shared
319  // `@factory/shared/realtime/notification-prefs` schema; absent types use
320  // the defaults there. Additive, default "{}" (no data loss).
321  notificationPrefs  Json          @default("{}")
322  // TASK-0133 — org `require2FA` policy. The TOTP seed is stored AES-256-GCM
323  // encrypted (base64 envelope, same scheme as SSO/webhook secrets); `null` =
324  // 2FA not enrolled. The raw seed is revealed once (enrollment QR/secret).
325  twoFactorSecret  String?
326  lastLoginAt        DateTime?
327  createdAt          DateTime      @default(now())
328  updatedAt          DateTime      @updatedAt
329 
330  memberships        WorkspaceMember[]
331  ownedWorkspaces    Workspace[]     @relation("WorkspaceOwner")
332  ownedOrganizations Organization[]  @relation("OrgOwner")
333  orgMemberships     OrganizationMember[]
334  // Confluence expansion (T-157/T-153/T-154):
335  pageWatches        PageWatch[]
336  recentItems        UserRecentItem[]
337  savedSearches      SavedSearch[]
338  comments           Comment[]
339  commentReplies     CommentReply[]
340  notifications      Notification[]
341  favorites          PageFavorite[]
342  pageVersions       PageVersion[]
343  blockVersions      BlockVersion[]
344  fileVersions       FileVersion[]
345  sessions           Session[]
346  refreshTokens      RefreshToken[]
347  passwordResets     PasswordReset[]
348  apiKeys            ApiKey[]
349  automations        Automation[]  @relation("AutomationCreator")
350  aiConversations    AiConversation[]
351  mentionNotifications MentionNotification[]
352 
353  @@map("users")
354}
355 
356model RefreshToken {
357  id           String        @id @default(cuid())
358  tokenHash    String        @unique
359  userId       String
360  user         User          @relation(fields: [userId], references: [id], onDelete: Cascade)
361  sessionAgent String?
362  createdAt    DateTime      @default(now())
363  expiresAt    DateTime
364  revokedAt    DateTime?
365  replacedBy   String?
366 
367  @@map("refresh_tokens")
368}
369 
370model Session {
371  id           String        @id @default(cuid())
372  userId       String
373  user         User          @relation(fields: [userId], references: [id], onDelete: Cascade)
374  tokenHash    String        @unique
375  userAgent    String?
376  ipAddress    String?
377  status       SessionStatus @default(ACTIVE)
378  createdAt    DateTime      @default(now())
379  expiresAt    DateTime
380  revokedAt    DateTime?
381 
382  @@index([userId, status])
383  @@map("sessions")
384}
385 
386model EmailVerification {
387  id        String   @id @default(cuid())
388  userId    String
389  tokenHash String   @unique
390  expiresAt DateTime
391  createdAt DateTime @default(now())
392 
393  @@index([userId])
394  @@map("email_verifications")
395}
396 
397model PasswordReset {
398  id        String              @id @default(cuid())
399  userId    String
400  user      User                @relation(fields: [userId], references: [id], onDelete: Cascade)
401  tokenHash String              @unique
402  status    PasswordResetStatus @default(PENDING)
403  expiresAt DateTime
404  usedAt    DateTime?
405  createdAt DateTime            @default(now())
406 
407  @@index([userId])
408  @@map("password_resets")
409}
410 
411model Organization {
412  id                String            @id @default(cuid())
413  name              String
414  slug              String            @unique
415  logoUrl           String?
416  ownerId           String
417  owner             User              @relation("OrgOwner", fields: [ownerId], references: [id])
418  policy            Json              @default("{}") // org policies: security, retention, SSO enforcement
419  ssoConfig         Json?
420  scimConfig        Json?
421  createdAt         DateTime          @default(now())
422  updatedAt         DateTime          @updatedAt
423 
424  members           OrganizationMember[]
425  workspaces        Workspace[]
426  ssoProvider       SsoProvider?
427  retentionPolicies RetentionPolicy[]
428  auditExports      AuditExport[]
429 
430  @@map("organizations")
431}
432 
433// TASK-0133 — async audit CSV exports (the >10k-row path; `exports/{orgId}/
434// {exportId}.csv` in the files bucket, retained 7 d). `status` is an app-
435// level String (processing|ready|failed — no enum: the worker + API share the
436// literals, mirroring the other app-level status fields). `downloadUsed` is
437// the single-use flag (the presigned URL is minted at most once; the
438// `?redownload=1` poll mints one more).
439model AuditExport {
440  id             String   @id // the exportId (uuid, created by the API)
441  organizationId String
442  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
443  status         String   @default("processing") // processing | ready | failed
444  totalRows      Int
445  capRows        Int
446  rows           Int      @default(0)
447  truncated      Boolean  @default(false) // true when capRows < totalRows
448  filters        Json     @default("{}")
449  error          String?
450  downloadUrlAt  DateTime?
451  downloadUsed   Boolean  @default(false)
452  createdAt      DateTime @default(now())
453  updatedAt      DateTime @updatedAt
454  completedAt    DateTime?
455 
456  @@index([organizationId, status])
457  @@index([organizationId, createdAt])
458  @@map("audit_exports")
459}
460 
461model OrganizationMember {
462  id             String          @id @default(cuid())
463  organizationId String
464  userId         String
465  role           WorkspaceRole   @default(MEMBER)
466  createdAt      DateTime        @default(now())
467 
468  organization   Organization    @relation(fields: [organizationId], references: [id], onDelete: Cascade)
469  user           User            @relation(fields: [userId], references: [id], onDelete: Cascade)
470 
471  @@unique([organizationId, userId])
472  @@map("organization_members")
473}
474 
475model SsoProvider {
476  id             String            @id @default(cuid())
477  organizationId String            @unique
478  type           SSOProviderType
479  name           String
480  config         Json              // provider config (entityId, certificate ref, issuer, client id)
481  enforce        Boolean           @default(false)
482  createdAt      DateTime          @default(now())
483  updatedAt      DateTime          @updatedAt
484 
485  organization   Organization      @relation(fields: [organizationId], references: [id], onDelete: Cascade)
486  identityLinks  SsoIdentity[]
487 
488  @@map("sso_providers")
489}
490 
491model SsoIdentity {
492  id         String      @id @default(cuid())
493  providerId String
494  externalId String
495  userId     String
496  createdAt  DateTime    @default(now())
497 
498  provider   SsoProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
499 
500  @@unique([providerId, externalId])
501  @@map("sso_identities")
502}
503 
504model RetentionPolicy {
505  id             String                @id @default(cuid())
506  organizationId String
507  type           RetentionPolicyType
508  scope          String                // "comments" | "trash" | "audit_logs" | "files" ...
509  retainDays     Int?
510  config         Json                  @default("{}")
511  createdAt      DateTime              @default(now())
512  updatedAt      DateTime              @updatedAt
513 
514  organization   Organization          @relation(fields: [organizationId], references: [id], onDelete: Cascade)
515 
516  // TASK-0133 — one policy per (org, scope) (retention engine + PATCH upsert).
517  @@unique([organizationId, scope])
518  @@map("retention_policies")
519}
520 
521model Workspace {
522  id               String          @id @default(cuid())
523  organizationId   String?
524  name             String
525  slug             String          @unique
526  iconUrl          String?
527  logoUrl          String?
528  description      String?
529  publicUrl        String?         // e.g. https://acme.factory.app
530  /// Phase 12 (TASK-0121): `settings.ai` — per-workspace AI config
531  /// (`{enabled?, provider?, chatModel?, embedModel?, budgets?, endpoint?}` —
532  /// the `@factory/ai` `AiConfigSchema` shape, per-field fallback onto the
533  /// env platform defaults `AI_PROVIDER`/`AI_BASE_URL`/`AI_API_KEY`/
534  /// `AI_MODEL`). Provider selection is configuration-only; swapping the
535  /// provider never changes application code (ROADMAP Phase 12 exit).
536  settings         Json            @default("{}")
537  /// Confluence expansion (T-136): space type — "team" | "project" | "kb" |
538  /// "custom". Legacy workspaces read as "team" (default). Drives the space
539  /// directory filter; no permission semantics attached.
540  type             String          @default("team")
541  /// Confluence expansion (T-136): soft archive (distinct from `deletedAt`
542  /// removal). Archived spaces are hidden from the directory; content and
543  /// membership are preserved; restore clears the flag.
544  archivedAt       DateTime?
545  isPersonal       Boolean         @default(false)
546  ownerId          String?
547  owner            User?           @relation("WorkspaceOwner", fields: [ownerId], references: [id], onDelete: SetNull)
548  billingPlan      String          @default("free")
549  /// Phase 12 (TASK-0121): `usageLimits.aiMonthlyTokens` — the per-workspace
550  /// monthly AI token budget (Int; unset or 0 = unlimited, plan default).
551  /// Enforced by the AI gateway at submit time → `AI_BUDGET_EXCEEDED` (402)
552  /// + a throttled admin notification (one per workspace per 24 h).
553  usageLimits      Json            @default("{}")
554  /// TASK-0104 §4: materialized storage-quota counter (bytes of non-deleted
555  /// files). Updated transactionally by the file service (complete +size,
556  /// purge / replace-old-versions −size; restore = no change) so session
557  /// pre-checks are O(1) — never a live sum. BigInt (PG bigint): 10 GB
558  /// workspaces overflow Int.
559  usageStorageBytes BigInt         @default(0)
560  createdAt        DateTime        @default(now())
561  updatedAt        DateTime        @updatedAt
562  deletedAt        DateTime?
563 
564  organization     Organization?   @relation(fields: [organizationId], references: [id], onDelete: SetNull)
565  members          WorkspaceMember[]
566  invitations      Invitation[]
567  teams            Team[]
568  pages            Page[]
569  databases        Database[]
570  files            File[]
571  automations      Automation[]
572  integrations     Integration[]
573  webhooks         Webhook[]
574  apiKeys          ApiKey[]
575  shares           Share[]
576  templates        Template[]
577  notifications    Notification[]
578  auditLogs        AuditLog[]
579  searchDocuments  SearchDocument[]
580  aiConversations  AiConversation[]
581  formSubmissions  FormSubmission[]
582  dashboards       Dashboard[]
583  comments         Comment[]
584 
585  @@index([organizationId])
586  @@map("workspaces")
587}
588 
589model WorkspaceMember {
590  id                String          @id @default(cuid())
591  workspaceId       String
592  userId            String
593  role              WorkspaceRole   @default(MEMBER)
594  invitedBy         String?
595  joinedAt          DateTime?
596  createdAt         DateTime        @default(now())
597  updatedAt         DateTime        @updatedAt
598 
599  workspace         Workspace       @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
600  user              User            @relation(fields: [userId], references: [id], onDelete: Cascade)
601  teamMemberships   TeamMember[]
602 
603  @@unique([workspaceId, userId])
604  @@index([userId])
605  @@map("workspace_members")
606}
607 
608model Invitation {
609  id           String            @id @default(cuid())
610  workspaceId  String
611  workspace    Workspace         @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
612  email        String
613  role         WorkspaceRole     @default(MEMBER)
614  invitedById  String
615  tokenHash    String            @unique
616  status       InvitationStatus  @default(PENDING)
617  expiresAt    DateTime
618  acceptedAt   DateTime?
619  revokedAt    DateTime?
620  createdAt    DateTime          @default(now())
621  updatedAt    DateTime          @updatedAt
622 
623  @@unique([workspaceId, email, role])
624  @@index([workspaceId, status])
625  @@map("invitations")
626}
627 
628model Team {
629  id           String      @id @default(cuid())
630  workspaceId  String
631  workspace    Workspace   @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
632  name         String
633  slug         String
634  description  String?
635  color        String?
636  iconUrl      String?
637  position     String      @default("a0") // fractional ordering in sidebar
638  isPrivate    Boolean     @default(false)
639  createdAt    DateTime    @default(now())
640  updatedAt    DateTime    @updatedAt
641 
642  members      TeamMember[]
643  pages        Page[]
644  comments     Comment[]
645  shares       Share[]
646 
647  @@unique([workspaceId, slug])
648  @@map("teams")
649}
650 
651model TeamMember {
652  id                  String          @id @default(cuid())
653  teamId              String
654  workspaceMemberId   String
655  userId              String
656  role                WorkspaceRole   @default(MEMBER)
657  createdAt           DateTime        @default(now())
658 
659  team                Team            @relation(fields: [teamId], references: [id], onDelete: Cascade)
660  workspaceMember     WorkspaceMember @relation(fields: [workspaceMemberId], references: [id], onDelete: Cascade)
661 
662  @@unique([teamId, userId])
663  @@map("team_members")
664}
665 
666// ============ PAGES & BLOCKS ============
667 
668model Page {
669  id            String       @id @default(cuid())
670  workspaceId   String
671  workspace     Workspace    @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
672  teamId        String?
673  team          Team?        @relation(fields: [teamId], references: [id], onDelete: SetNull)
674  parentId      String?
675  parent        Page?        @relation("PageTree", fields: [parentId], references: [id], onDelete: SetNull)
676  children      Page[]       @relation("PageTree")
677  grandparentId String?      // denormalized parent's parent for fast sibling queries
678  position      String       @default("a0") // fractional ordering
679  title         String       @default("")
680  icon          String?      // emoji or file id
681  coverUrl      String?
682  description   String?
683  /// Confluence expansion (T-144/A-001/A-006): content-type discriminator —
684  /// "page" | "whiteboard" | "task". Legacy pages read as "page" (default);
685  /// the page tree, permissions, versions, trash, and search stay unified.
686  /// Whiteboards carry their canvas in the 1:1 `whiteboards` sidecar; tasks
687  /// carry queryable columns in the 1:1 `tasks` sidecar.
688  kind          String       @default("page")
689  /// Confluence expansion (T-162): review workflow state — "draft" |
690  /// "in_review" | "approved" | "published" | "changes_requested" |
691  /// "archived". Legacy pages read as "published" (default = no behavior
692  /// change). Transitions are validated server-side in the review service.
693  reviewState   String       @default("published")
694  isPrivate     Boolean      @default(false)
695  isFavorite    Boolean      @default(false)
696  archivedAt    DateTime?
697  deletedAt     DateTime?
698  trashedAt     DateTime?
699  restoredAt    DateTime?
700  createdBy     String?
701  updatedBy     String?
702  version       Int          @default(1) // optimistic concurrency
703  createdAt     DateTime     @default(now())
704  updatedAt     DateTime     @updatedAt
705 
706  blocks        Block[]
707  favorites     PageFavorite[]
708  versions      PageVersion[]
709  comments      Comment[]
710  shares        Share[]
711  record        DatabaseRecord?
712  auditLogs     AuditLog[]
713  reviews       Review[]
714  watch         PageWatch[]
715  whiteboard    Whiteboard?
716  task          Task?
717 
718  @@index([workspaceId, parentId, position])
719  @@index([workspaceId, deletedAt])
720  @@index([teamId])
721  @@map("pages")
722}
723 
724model PageFavorite {
725  id          String   @id @default(cuid())
726  workspaceId String
727  userId      String
728  pageId      String
729  position    String   @default("a0")
730  createdAt   DateTime @default(now())
731 
732  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
733  page        Page     @relation(fields: [pageId], references: [id], onDelete: Cascade)
734 
735  @@unique([workspaceId, userId, pageId])
736  @@map("page_favorites")
737}
738 
739model PageVersion {
740  id        String   @id @default(cuid())
741  pageId    String
742  version   Int
743  snapshot  Json     // page + block tree snapshot
744  createdBy String?
745  note      String?
746  createdAt DateTime @default(now())
747 
748  page      Page     @relation(fields: [pageId], references: [id], onDelete: Cascade)
749  user      User?    @relation(fields: [createdBy], references: [id], onDelete: SetNull)
750 
751  @@unique([pageId, version])
752  @@map("page_versions")
753}
754 
755model Block {
756  id             String    @id @default(cuid())
757  pageId         String
758  workspaceId    String
759  parentId       String?
760  type           BlockType
761  position       String    @default("a0") // fractional ordering, NEVER array indexes
762  content        Json      @default("{}") // type-specific content (text, media refs, code, ...)
763  properties     Json      @default("{}") // colors, alignment, column width, checkbox state, ...
764  isSynced       Boolean   @default(false)
765  syncedSourceId String?   // for SYNCED_BLOCK: source block id
766  createdBy      String?
767  updatedBy      String?
768  deletedAt      DateTime?
769  version        Int       @default(1)
770  createdAt      DateTime  @default(now())
771  updatedAt      DateTime  @updatedAt
772 
773  page           Page      @relation(fields: [pageId], references: [id], onDelete: Cascade)
774  versions       BlockVersion[]
775  comments       Comment[]
776  files          File[]
777 
778  @@index([pageId, parentId, position])
779  @@index([pageId, position]) // TASK-0135: tree depth=all load (EXPLAIN-proven)
780  @@index([workspaceId, pageId])
781  @@map("blocks")
782}
783 
784model BlockVersion {
785  id         String   @id @default(cuid())
786  blockId    String
787  version    Int
788  content    Json
789  properties Json
790  createdBy  String?
791  createdAt  DateTime @default(now())
792 
793  block      Block    @relation(fields: [blockId], references: [id], onDelete: Cascade)
794  user       User?    @relation(fields: [createdBy], references: [id], onDelete: SetNull)
795 
796  @@unique([blockId, version])
797  @@map("block_versions")
798}
799 
800// ============ DATABASES ============
801 
802model Database {
803  id            String    @id @default(cuid())
804  workspaceId   String
805  workspace     Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
806  pageId        String?   // page the database block is embedded in (plain ref)
807  title         String    @default("")
808  icon          String?
809  description   String?
810  schemaVersion Int       @default(1)
811  isArchived    Boolean   @default(false)
812  createdBy     String?
813  createdAt     DateTime  @default(now())
814  updatedAt     DateTime  @updatedAt
815 
816  properties    DatabaseProperty[]
817  views         DatabaseView[]
818  records       DatabaseRecord[]
819  relations     Relation[]
820  relationLinks RelationLink[]
821  rollups       Rollup[]
822  formulas      Formula[]
823  buttons       DatabaseButton[]
824  forms         Form[]
825  charts        Chart[]
826 
827  @@index([workspaceId])
828  @@map("databases")
829}
830 
831model DatabaseProperty {
832  id         String       @id @default(cuid())
833  databaseId String
834  name       String
835  type       PropertyType
836  position   String       @default("a0")
837  isTitle    Boolean      @default(false)
838  isVisible  Boolean      @default(true)
839  required   Boolean      @default(false)
840  config     Json         @default("{}") // options[], format, relation ref, formula ref, button config
841  default    Json?
842  deletedAt  DateTime?
843  createdAt  DateTime     @default(now())
844  updatedAt  DateTime     @updatedAt
845 
846  database   Database     @relation(fields: [databaseId], references: [id], onDelete: Cascade)
847  values     RecordProperty[]
848 
849  @@unique([databaseId, name])
850  @@index([databaseId, position])
851  @@map("database_properties")
852}
853 
854model DatabaseView {
855  id         String   @id @default(cuid())
856  databaseId String
857  name       String
858  type       ViewType
859  position   String   @default("a0")
860  config     Json     @default("{}") // visible properties, column widths, card layout, color, density
861  filter     Json?    // nested AND/OR/NOT filter tree
862  sort       Json?    // [{propertyId, direction}]
863  group      Json?    // [{propertyId, direction}]
864  pageSize   Int?
865  createdAt  DateTime @default(now())
866  updatedAt  DateTime @updatedAt
867 
868  database   Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
869 
870  @@index([databaseId, position])
871  @@map("database_views")
872}
873 
874model DatabaseRecord {
875  id             String       @id @default(cuid())
876  workspaceId    String
877  databaseId     String
878  pageId         String?      @unique // each record IS a page
879  coverUrl       String?
880  icon           String?
881  archivedAt     DateTime?
882  isSubItem      Boolean      @default(false)
883  position       String       @default("a0") // ordering inside table/board views
884  version        Int          @default(1)
885  createdBy      String?
886  createdTime    DateTime     @default(now())
887  lastEditedBy   String?
888  lastEditedTime DateTime     @default(now())
889  createdAt      DateTime     @default(now())
890  updatedAt      DateTime     @updatedAt
891 
892  database       Database     @relation(fields: [databaseId], references: [id], onDelete: Cascade)
893  page           Page?        @relation(fields: [pageId], references: [id], onDelete: SetNull)
894  properties     RecordProperty[]
895  relationLinks  RelationLink[]
896  formSubmissions FormSubmission[]
897 
898  @@index([workspaceId, databaseId, position])
899  @@index([databaseId, archivedAt])
900  @@index([databaseId, position]) // TASK-0135: 100k-record table list (EXPLAIN-proven)
901  @@map("database_records")
902}
903 
904model RecordProperty {
905  id         String   @id @default(cuid())
906  recordId   String
907  propertyId String
908  value      Json?    // typed value per property.type (select option ids, file ids, user ids)
909  createdAt  DateTime @default(now())
910  updatedAt  DateTime @updatedAt
911 
912  record     DatabaseRecord   @relation(fields: [recordId], references: [id], onDelete: Cascade)
913  property   DatabaseProperty @relation(fields: [propertyId], references: [id], onDelete: Cascade)
914 
915  @@unique([recordId, propertyId])
916  @@map("record_properties")
917}
918 
919// ============ RELATIONS / ROLLUPS / FORMULAS / BUTTONS ============
920 
921model Relation {
922  id                String              @id @default(cuid())
923  databaseId        String
924  targetDatabaseId  String
925  propertyId        String?             // relation property on this database
926  reversePropertyId String?             // bidirectional counterpart
927  name              String
928  targetName        String?
929  cardinality       RelationCardinality @default(MANY_TO_MANY)
930  bidirectional     Boolean             @default(true)
931  createdAt         DateTime            @default(now())
932  updatedAt         DateTime            @updatedAt
933 
934  database          Database            @relation(fields: [databaseId], references: [id], onDelete: Cascade)
935 
936  @@index([databaseId])
937  @@index([targetDatabaseId])
938  @@map("relations")
939}
940 
941model RelationLink {
942  id               String   @id @default(cuid())
943  databaseId       String
944  recordId         String
945  targetRecordId   String
946  targetDatabaseId String
947  createdAt        DateTime @default(now())
948 
949  database         Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
950  record           DatabaseRecord @relation(fields: [recordId], references: [id], onDelete: Cascade)
951 
952  @@unique([databaseId, recordId, targetDatabaseId, targetRecordId])
953  @@index([targetDatabaseId, targetRecordId])
954  @@map("relation_links")
955}
956 
957model Rollup {
958  id                 String          @id @default(cuid())
959  databaseId         String
960  propertyId         String?         // the ROLLUP property on this database
961  relationId         String
962  targetPropertyId   String
963  aggregate          RollupAggregate
964  filter             Json?
965  createdAt          DateTime        @default(now())
966  updatedAt          DateTime        @updatedAt
967 
968  database           Database        @relation(fields: [databaseId], references: [id], onDelete: Cascade)
969 
970  @@index([databaseId])
971  @@map("rollups")
972}
973 
974model Formula {
975  id           String   @id @default(cuid())
976  databaseId   String
977  propertyId   String?
978  expression   String   // source text, e.g. if(prop("Status") == "Done", 100, 0)
979  ast          Json     // parsed AST (safe evaluator input)
980  returnType   String   // inferred return type
981  dependencies String[] // property ids this formula references
982  createdAt    DateTime @default(now())
983  updatedAt    DateTime @updatedAt
984 
985  database     Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
986 
987  @@index([databaseId])
988  @@map("formulas")
989}
990 
991model DatabaseButton {
992  id         String   @id @default(cuid())
993  databaseId String
994  propertyId String?
995  name       String
996  icon       String?
997  color      String?
998  actions    Json     // [{type, config}] — set property, create page, webhook, automation
999  createdAt  DateTime @default(now())
1000  updatedAt  DateTime @updatedAt
1001 
1002  database   Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
1003 
1004  @@index([databaseId])
1005  @@map("database_buttons")
1006}
1007 
1008// ============ CHARTS & DASHBOARDS ============
1009 
1010model Chart {
1011  id          String   @id @default(cuid())
1012  workspaceId String
1013  databaseId  String
1014  name        String
1015  chartType   String   // bar | line | area | pie | donut | number
1016  config      Json     // x axis, y axis, group, aggregation, color, legend
1017  filter      Json?
1018  position    String   @default("a0")
1019  createdAt   DateTime @default(now())
1020  updatedAt   DateTime @updatedAt
1021 
1022  database    Database @relation(fields: [databaseId], references: [id], onDelete: Cascade)
1023 
1024  @@index([workspaceId, databaseId])
1025  @@map("charts")
1026}
1027 
1028model Dashboard {
1029  id          String    @id @default(cuid())
1030  workspaceId String
1031  workspace   Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1032  name        String
1033  icon        String?
1034  layout      Json      @default("{}") // widget grid: [{widgetType, sourceRef, x, y, w, h, config}]
1035  isPublic    Boolean   @default(false)
1036  createdAt   DateTime  @default(now())
1037  updatedAt   DateTime  @updatedAt
1038 
1039  widgets     DashboardWidget[]
1040 
1041  @@index([workspaceId])
1042  @@map("dashboards")
1043}
1044 
1045model DashboardWidget {
1046  id         String    @id @default(cuid())
1047  dashboardId String
1048  widgetType String    // table | board | chart | number | calendar | timeline | text | linked_database
1049  sourceRef  Json?     // {type: "database"|"chart"|"view", id}
1050  position   String    @default("a0")
1051  layout     Json      @default("{}")
1052  filter     Json?
1053  createdAt  DateTime  @default(now())
1054  updatedAt  DateTime  @updatedAt
1055 
1056  dashboard  Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
1057 
1058  @@index([dashboardId, position])
1059  @@map("dashboard_widgets")
1060}
1061 
1062// ============ COMMENTS & NOTIFICATIONS ============
1063 
1064model Comment {
1065  id             String    @id @default(cuid())
1066  workspaceId    String
1067  teamId         String?
1068  pageId         String?
1069  blockId        String?
1070  recordId       String?   // database record (plain ref)
1071  selection      Json?     // text selection anchor/offset
1072  body           String
1073  mentionIds     String[]  @default([]) // user ids mentioned
1074  pageMentionIds String[]  @default([]) // pages/databases mentioned (plain refs)
1075  resolvedAt     DateTime?
1076  resolvedBy     String?
1077  status         String    @default("open") // open | resolved | reopened
1078  createdBy      String
1079  createdAt      DateTime  @default(now())
1080  updatedAt      DateTime  @updatedAt
1081 
1082  workspace      Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1083  team           Team?     @relation(fields: [teamId], references: [id], onDelete: SetNull)
1084  page           Page?     @relation(fields: [pageId], references: [id], onDelete: Cascade)
1085  block          Block?    @relation(fields: [blockId], references: [id], onDelete: Cascade)
1086  author         User      @relation(fields: [createdBy], references: [id], onDelete: Cascade)
1087  replies        CommentReply[]
1088  reactions      CommentReaction[]
1089 
1090  @@index([workspaceId, pageId])
1091  @@index([workspaceId, blockId])
1092  @@index([workspaceId, recordId])
1093  @@map("comments")
1094}
1095 
1096model CommentReply {
1097  id         String   @id @default(cuid())
1098  commentId  String
1099  body       String
1100  mentionIds String[] @default([])
1101  createdBy  String
1102  createdAt  DateTime @default(now())
1103  updatedAt  DateTime @updatedAt
1104 
1105  comment    Comment  @relation(fields: [commentId], references: [id], onDelete: Cascade)
1106  author     User     @relation(fields: [createdBy], references: [id], onDelete: Cascade)
1107  reactions  CommentReaction[]
1108 
1109  @@index([commentId])
1110  @@map("comment_replies")
1111}
1112 
1113model CommentReaction {
1114  id        String    @id @default(cuid())
1115  emoji     String
1116  userId    String
1117  commentId String?
1118  replyId   String?
1119  createdAt DateTime  @default(now())
1120 
1121  comment   Comment?    @relation(fields: [commentId], references: [id], onDelete: Cascade)
1122  reply     CommentReply? @relation(fields: [replyId], references: [id], onDelete: Cascade)
1123 
1124  @@unique([commentId, replyId, userId, emoji])
1125  @@map("comment_reactions")
1126}
1127 
1128model Notification {
1129  id          String              @id @default(cuid())
1130  workspaceId String
1131  userId      String
1132  type        NotificationType
1133  channel     NotificationChannel @default(IN_APP)
1134  title       String
1135  body        String?
1136  link        Json?               // deep link {type, id}
1137  readAt      DateTime?
1138  // The acting user (the notification's author, e.g. the mentioner / the
1139  // person who replied). Used for: the self-actor email suppression
1140  // (TASK-0094 R6: actorId == userId → no email) + the bell's actor label.
1141  actorId     String?
1142  // EMAIL idempotency (TASK-0094 R6): set by the worker when the email
1143  // job succeeds; a double-enqueue (the BullMQ job-key race) is guarded by
1144  // the unique job key `email_{notificationId}` + this flag. (Colon-free:
1145  // BullMQ 5.x rejects custom jobIds containing `:` unless 3-segment — the
1146  // old `email:{id}` threw on every enqueue; fixed 2026-10-12.)
1147  emailSentAt DateTime?
1148  createdAt   DateTime            @default(now())
1149 
1150  workspace   Workspace           @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1151  user        User                @relation(fields: [userId], references: [id], onDelete: Cascade)
1152 
1153  @@index([userId, readAt])
1154  @@index([workspaceId, userId])
1155  @@map("notifications")
1156}
1157 
1158model MentionNotification {
1159  id          String   @id @default(cuid())
1160  userId      String
1161  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
1162  mentionerId String
1163  targetType  String   // comment | reply | page | record
1164  targetId    String
1165  context     Json?
1166  readAt      DateTime?
1167  createdAt   DateTime @default(now())
1168 
1169  @@index([userId, readAt])
1170  @@map("mention_notifications")
1171}
1172 
1173// ============ FILES ============
1174 
1175model File {
1176  id           String     @id @default(cuid())
1177  workspaceId  String
1178  workspace    Workspace  @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1179  blockId      String?
1180  pageId       String?    // plain ref
1181  name         String
1182  key          String     @unique // object storage key
1183  url          String?    // public/proxy URL
1184  size         Int
1185  mimeType     String
1186  status       FileStatus @default(UPLOADING)
1187  metadata     Json       @default("{}") // dimensions, duration, page count, hashes
1188  thumbnailKey String?
1189  thumbnailUrl String?
1190  scanReport   Json?      // virus scan result
1191  uploadedBy   String
1192  permissions  Json?      // file-level permission overrides
1193  deletedAt    DateTime?
1194  createdAt    DateTime   @default(now())
1195  updatedAt    DateTime   @updatedAt
1196 
1197  block        Block?     @relation(fields: [blockId], references: [id], onDelete: SetNull)
1198  versions     FileVersion[]
1199 
1200  @@index([workspaceId, deletedAt])
1201  @@index([blockId])
1202  // TASK-0135 — workspace file listing (ORDER BY createdAt DESC), EXPLAIN-proven.
1203  @@index([workspaceId, createdAt(sort: Desc)])
1204  @@map("files")
1205}
1206 
1207model FileVersion {
1208  id        String   @id @default(cuid())
1209  fileId    String
1210  version   Int
1211  key       String
1212  size      Int
1213  createdBy String?
1214  createdAt DateTime @default(now())
1215 
1216  file      File     @relation(fields: [fileId], references: [id], onDelete: Cascade)
1217  user      User?    @relation(fields: [createdBy], references: [id], onDelete: SetNull)
1218 
1219  @@unique([fileId, version])
1220  @@map("file_versions")
1221}
1222 
1223// ============ PERMISSIONS & SHARING ============
1224 
1225model Permission {
1226  id             String          @id @default(cuid())
1227  workspaceId    String
1228  granteeType    String          // "user" | "team" | "role"
1229  granteeId      String          // userId | teamId | role name (plain refs)
1230  resourceType   String          // "workspace" | "teamspace" | "page" | "database" | "record" | "file"
1231  resourceId     String
1232  level          PermissionLevel
1233  inheritsFrom   String?         // parent grant id for inheritance chains
1234  createdAt      DateTime        @default(now())
1235  updatedAt      DateTime        @updatedAt
1236 
1237  @@unique([workspaceId, granteeType, granteeId, resourceType, resourceId])
1238  @@index([workspaceId, granteeId])
1239  @@index([resourceType, resourceId])
1240  @@map("permissions")
1241}
1242 
1243model Share {
1244  id                  String     @id @default(cuid())
1245  workspaceId         String
1246  pageId              String?
1247  teamId              String?
1248  recordId            String?    // plain ref
1249  token               String     @unique
1250  scope               ShareScope @default(PRIVATE)
1251  allowComments       Boolean    @default(true)
1252  allowDuplicate      Boolean    @default(false)
1253  allowSearchIndexing Boolean    @default(false)
1254  passwordHash        String?
1255  expiresAt           DateTime?
1256  createdBy           String
1257  createdAt           DateTime   @default(now())
1258  updatedAt           DateTime   @updatedAt
1259 
1260  workspace           Workspace  @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1261  page                Page?      @relation(fields: [pageId], references: [id], onDelete: Cascade)
1262  team                Team?      @relation(fields: [teamId], references: [id], onDelete: Cascade)
1263 
1264  @@index([workspaceId, pageId])
1265  @@index([token])
1266  @@map("shares")
1267}
1268 
1269// ============ TEMPLATES ============
1270 
1271model Template {
1272  id               String    @id @default(cuid())
1273  workspaceId      String
1274  workspace        Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1275  name             String
1276  icon             String?
1277  type             String    // "workspace" | "database" | "page" | "team"
1278  sourceRef        Json?     // {type, id} of the template source (plain refs)
1279  sourceDatabaseId String?   // denormalized, plain ref
1280  snapshot         Json      // frozen page/database structure for instantiation
1281  isBuiltin        Boolean   @default(false)
1282  /// Confluence expansion (T-141/T-142): categories/descriptions drive the
1283  /// directory grouping + search; `variables` is the typed variable
1284  /// definition list for form-based instantiation (plain-text substitution
1285  /// only — never interpreted as code/HTML); `defaultFor` marks the
1286  /// workspace default template for new content ("page" | "database" | null).
1287  category         String?
1288  description      String?
1289  variables        Json?
1290  defaultFor       String?
1291  createdBy        String?
1292  createdAt        DateTime  @default(now())
1293  updatedAt        DateTime  @updatedAt
1294 
1295  versions         TemplateVersion[]
1296 
1297  @@index([workspaceId, type])
1298  @@map("templates")
1299}
1300 
1301model TemplateVersion {
1302  id          String   @id @default(cuid())
1303  templateId  String
1304  version     Int
1305  snapshot    Json
1306  name        String?
1307  description String?
1308  variables   Json?
1309  note        String?
1310  createdBy   String?
1311  createdAt   DateTime @default(now())
1312 
1313  template    Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
1314 
1315  @@unique([templateId, version])
1316  @@map("template_versions")
1317}
1318 
1319// ============ AUTOMATION ============
1320 
1321model Automation {
1322  id          String             @id @default(cuid())
1323  workspaceId String
1324  workspace   Workspace          @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1325  name        String
1326  description String?
1327  status      AutomationStatus   @default(DRAFT)
1328  databaseId  String?            // scoped database for record triggers (plain ref)
1329  createdById String
1330  createdBy   User               @relation("AutomationCreator", fields: [createdById], references: [id])
1331  createdAt   DateTime           @default(now())
1332  updatedAt   DateTime           @updatedAt
1333 
1334  triggers    AutomationTrigger[]
1335  conditions  AutomationCondition[]
1336  actions     AutomationAction[]
1337  runs        AutomationRun[]
1338 
1339  @@index([workspaceId, status])
1340  @@map("automations")
1341}
1342 
1343model AutomationTrigger {
1344  id           String                @id @default(cuid())
1345  automationId String
1346  type         AutomationTriggerType
1347  config       Json                  // property, value, cron expression, webhook id...
1348  position     Int                   @default(0)
1349 
1350  automation   Automation            @relation(fields: [automationId], references: [id], onDelete: Cascade)
1351 
1352  @@index([automationId])
1353  @@map("automation_triggers")
1354}
1355 
1356model AutomationCondition {
1357  id           String   @id @default(cuid())
1358  automationId String
1359  expression   Json     // condition tree (AND/OR/NOT over property predicates)
1360  position     Int      @default(0)
1361 
1362  automation   Automation @relation(fields: [automationId], references: [id], onDelete: Cascade)
1363 
1364  @@index([automationId])
1365  @@map("automation_conditions")
1366}
1367 
1368model AutomationAction {
1369  id           String               @id @default(cuid())
1370  automationId String
1371  type         AutomationActionType
1372  config       Json                 // target property, page template, email template, webhook id...
1373  position     Int
1374  retries      Int                  @default(3)
1375  timeoutMs    Int                  @default(30000)
1376 
1377  automation   Automation           @relation(fields: [automationId], references: [id], onDelete: Cascade)
1378  runs         AutomationRun[]
1379 
1380  @@index([automationId, position])
1381  @@map("automation_actions")
1382}
1383 
1384model AutomationRun {
1385  id           String               @id @default(cuid())
1386  workspaceId  String
1387  automationId String
1388  actionId     String?
1389  status       AutomationRunStatus  @default(PENDING)
1390  triggerEvent Json?
1391  input        Json?
1392  output       Json?
1393  error        Json?
1394  startedAt    DateTime?
1395  finishedAt   DateTime?
1396  retryCount   Int                  @default(0)
1397  createdAt    DateTime             @default(now())
1398 
1399  automation   Automation           @relation(fields: [automationId], references: [id], onDelete: Cascade)
1400  action       AutomationAction?    @relation(fields: [actionId], references: [id], onDelete: SetNull)
1401  logs         AutomationLog[]
1402 
1403  @@index([workspaceId, automationId, createdAt])
1404  @@index([status])
1405  @@map("automation_runs")
1406}
1407 
1408model AutomationLog {
1409  id        String   @id @default(cuid())
1410  runId     String
1411  level     String   // info | warn | error
1412  message   String
1413  data      Json?
1414  createdAt DateTime @default(now())
1415 
1416  run       AutomationRun @relation(fields: [runId], references: [id], onDelete: Cascade)
1417 
1418  @@index([runId])
1419  @@map("automation_logs")
1420}
1421 
1422// ============ INTEGRATIONS / API / WEBHOOKS ============
1423 
1424model Integration {
1425  id          String   @id @default(cuid())
1426  workspaceId String
1427  name        String
1428  provider    String   // "slack" | "github" | "custom"...
1429  config      Json     // encrypted connection settings (tokens)
1430  status      String   @default("active")
1431  createdById String
1432  createdAt   DateTime @default(now())
1433  updatedAt   DateTime @updatedAt
1434 
1435  workspace   Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1436 
1437  @@index([workspaceId])
1438  @@map("integrations")
1439}
1440 
1441model ApiKey {
1442  id          String        @id @default(cuid())
1443  workspaceId String
1444  name        String
1445  keyHash     String        @unique // only hash stored; raw key shown once at creation
1446  prefix      String        // e.g. "fk_live_ab12..." for identification
1447  scopes      ApiKeyScope[]
1448  rateLimit   Int?
1449  lastUsedAt  DateTime?
1450  expiresAt   DateTime?
1451  revokedAt   DateTime?
1452  createdById String
1453  createdAt   DateTime      @default(now())
1454 
1455  workspace   Workspace     @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1456  createdBy   User          @relation(fields: [createdById], references: [id], onDelete: Cascade)
1457 
1458  @@index([workspaceId])
1459  @@map("api_keys")
1460}
1461 
1462model Webhook {
1463  id          String    @id @default(cuid())
1464  workspaceId String
1465  workspace   Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1466  url         String
1467  /// SHA-256 (hex) of the signing secret — bookkeeping + the incoming-
1468  /// verification path. NEVER returned by the API. TASK-0130: for legacy
1469  /// rows (pre-`secretEncrypted`) this may instead hold the AES-GCM
1470  /// ciphertext or a bare hash — see `resolveSigningSecret` (C-13.8a).
1471  secretHash String
1472  /// TASK-0130 — the RAW signing secret, AES-256-GCM-encrypted with
1473  /// MASTER_ENCRYPTION_KEY, base64(iv:tag:ciphertext). Present on every
1474  /// webhook created after this column landed (and backfilled for the
1475  /// Phase-11 rows, whose secret lived in `secretHash`).
1476  secretEncrypted Bytes?
1477  events      String[]  // subscribed event types
1478  active      Boolean   @default(true)
1479  createdById String
1480  createdAt   DateTime  @default(now())
1481  updatedAt   DateTime  @updatedAt
1482 
1483  deliveries  WebhookDelivery[]
1484 
1485  @@index([workspaceId])
1486  @@map("webhooks")
1487}
1488 
1489model WebhookDelivery {
1490  id          String                @id @default(cuid())
1491  webhookId   String
1492  event       String
1493  payload     Json
1494  status      WebhookDeliveryStatus @default(PENDING)
1495  attempts    Int                   @default(0)
1496  statusCode  Int?
1497  error       String?
1498  deliveredAt DateTime?
1499  nextRetryAt DateTime?
1500  createdAt   DateTime              @default(now())
1501  /// TASK-0130 — wall-clock ms of the LAST delivery attempt (docs §6 rows
1502  /// carry `latencyMs`; null until the first attempt completes).
1503  latencyMs   Int?
1504  /// TASK-0130 — set when this delivery was created by a RESUBMIT: the id of
1505  /// the original delivery it re-delivers (resubmit resets the attempt
1506  /// counter on the new row; the original stays put for the log).
1507  originDeliveryId String?
1508 
1509  webhook     Webhook               @relation(fields: [webhookId], references: [id], onDelete: Cascade)
1510 
1511  @@index([webhookId, status])
1512  @@index([status, nextRetryAt])
1513  /// TASK-0130 — inspector pagination (newest-first per webhook).
1514  @@index([webhookId, createdAt])
1515  @@map("webhook_deliveries")
1516}
1517 
1518// ============ AUDIT ============
1519 
1520model AuditLog {
1521  id           String        @id @default(cuid())
1522  actorType    AuditActorType
1523  actorId      String        // plain ref: userId, apiKey id, "system" or integration id
1524  workspaceId  String?
1525  pageId       String?
1526  resourceType String
1527  resourceId   String
1528  action       String        // "page.created", "permission.changed", "user.login", ...
1529  ipAddress    String?
1530  userAgent    String?
1531  metadata     Json          @default("{}")
1532  createdAt    DateTime      @default(now())
1533 
1534  workspace    Workspace?    @relation(fields: [workspaceId], references: [id], onDelete: SetNull)
1535  page         Page?         @relation(fields: [pageId], references: [id], onDelete: SetNull)
1536 
1537  @@index([workspaceId, action, createdAt])
1538  @@index([resourceType, resourceId, createdAt])
1539  @@index([actorType, actorId, createdAt])
1540  // TASK-0135 — workspace-wide audit listing (ORDER BY createdAt DESC), EXPLAIN-proven.
1541  @@index([workspaceId, createdAt(sort: Desc)])
1542  @@map("audit_logs")
1543}
1544 
1545// ============ SEARCH ============
1546 
1547model SearchDocument {
1548  id          String    @id @default(cuid())
1549  workspaceId String
1550  type        String    // "page" | "block" | "database" | "record" | "comment" | "file" | "user"
1551  refId       String
1552  title       String
1553  body        String?
1554  permissions Json      // visibility snapshot for query-time permission filtering
1555  metadata    Json      @default("{}")
1556  indexedAt   DateTime  @default(now())
1557  // SHA-1(title + "\n" + body + "\n" + permissions + "\n" + metadata) of the
1558  // SOURCE content at index time — the reconciliation anchor (search §1.3/§7.3).
1559  contentHash String?
1560 
1561  workspace   Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1562 
1563  @@unique([type, refId])
1564  @@index([workspaceId, type])
1565  @@map("search_documents")
1566}
1567 
1568// ============ AI ============
1569 
1570model AiConversation {
1571  id          String    @id @default(cuid())
1572  workspaceId String
1573  userId      String
1574  title       String?
1575  context     Json?     // {pageId, blockId, databaseId, ...}
1576  /// Phase 12 (TASK-0122): soft delete — the conversation hides from the
1577  /// list immediately and the maintenance purge job hard-deletes after 30 d
1578  /// (the existing trash convention).
1579  deletedAt   DateTime?
1580  createdAt   DateTime  @default(now())
1581  updatedAt   DateTime  @updatedAt
1582 
1583  workspace   Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1584  user        User      @relation(fields: [userId], references: [id], onDelete: Cascade)
1585  messages    AiMessage[]
1586 
1587  @@index([workspaceId, userId])
1588  @@map("ai_conversations")
1589}
1590 
1591model AiMessage {
1592  id             String   @id @default(cuid())
1593  conversationId String
1594  role           String   // user | assistant | system | tool
1595  content        String
1596  toolCalls      Json?
1597  toolResults    Json?
1598  /// Phase 12 (TASK-0121/0123+): per-message metadata — the feature tag
1599  /// for hidden per-call conversations (`{feature:"writing"|"rag"|"generate"…}`),
1600  /// the truncated flag on aborted streams (`{truncated:true}`), …
1601  metadata       Json?
1602  model          String?
1603  tokensIn       Int?
1604  tokensOut      Int?
1605  latencyMs      Int?
1606  createdAt      DateTime @default(now())
1607 
1608  conversation   AiConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
1609 
1610  /// Phase 12 (TASK-0121): `ai_messages` is the usage ledger — the monthly
1611  /// token rollup sums `tokensIn + tokensOut` per workspace since month-
1612  /// start, so the rollup's range scan needs the createdAt index below.
1613  @@index([conversationId])
1614  @@index([createdAt])
1615  @@index([conversationId, createdAt])
1616  @@map("ai_messages")
1617}
1618 
1619model AiEmbedding {
1620  id          String   @id @default(cuid())
1621  workspaceId String
1622  chunkRef    String   // {type, id} of the source chunk (plain ref)
1623  chunkText   String
1624  vectorId    String   // vector store document id
1625  model       String
1626  dimension   Int
1627  metadata    Json     @default("{}")
1628  createdAt   DateTime @default(now())
1629 
1630  /// Phase 12 (TASK-0125): the nightly re-embed walk scans per model —
1631  /// `@@index([workspaceId, model])` keeps that walk indexed.
1632  @@unique([workspaceId, chunkRef])
1633  @@index([workspaceId])
1634  @@index([workspaceId, model])
1635  @@map("ai_embeddings")
1636}
1637 
1638// ============ FORMS ============
1639 
1640model Form {
1641  id          String     @id @default(cuid())
1642  workspaceId String
1643  databaseId  String
1644  name        String
1645  slug        String     @unique
1646  description String?
1647  status      FormStatus @default(DRAFT)
1648  config      Json       @default("{}") // branding, intro, success message, closed message
1649  createdById String
1650  createdAt   DateTime   @default(now())
1651  updatedAt   DateTime   @updatedAt
1652 
1653  database    Database   @relation(fields: [databaseId], references: [id], onDelete: Cascade)
1654  questions   FormQuestion[]
1655  submissions FormSubmission[]
1656 
1657  @@index([workspaceId, databaseId])
1658  @@map("forms")
1659}
1660 
1661model FormQuestion {
1662  id         String             @id @default(cuid())
1663  formId     String
1664  label      String
1665  description String?
1666  type       FormQuestionType
1667  required   Boolean            @default(false)
1668  validation Json?              // min/max, pattern, options...
1669  propertyId String?            // maps to database property
1670  placeholder String?
1671  position   String             @default("a0")
1672  createdAt  DateTime           @default(now())
1673  updatedAt  DateTime           @updatedAt
1674 
1675  form       Form               @relation(fields: [formId], references: [id], onDelete: Cascade)
1676 
1677  @@index([formId, position])
1678  @@map("form_questions")
1679}
1680 
1681model FormSubmission {
1682  id          String   @id @default(cuid())
1683  workspaceId String
1684  formId      String
1685  recordId    String?  // created database record
1686  answers     Json     // {questionId: value}
1687  submittedBy String?  // null for anonymous public submissions
1688  ipAddress   String?
1689  createdAt   DateTime @default(now())
1690 
1691  workspace   Workspace       @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
1692  form        Form            @relation(fields: [formId], references: [id], onDelete: Cascade)
1693  record      DatabaseRecord? @relation(fields: [recordId], references: [id], onDelete: SetNull)
1694 
1695  @@index([workspaceId, formId, createdAt])
1696  @@map("form_submissions")
1697}
1698 
1699// ============ CONFLUENCE EXPANSION (T-144, additive) ============
1700// See docs/architecture/DATABASE_IMPACT.md + docs/implementation/DECISIONS.md
1701// (A-001 page-kind whiteboard, A-006 page-backed tasks, A-007 audit-projection
1702// activity feeds — so NO activity table here).
1703 
1704model Whiteboard {
1705  id           String   @id @default(cuid())
1706  pageId       String   @unique
1707  canvas       Json     // versioned canvas document ({schemaVersion, viewport, objects[]})
1708  schemaVersion Int     @default(1)
1709  updatedBy    String?
1710  createdAt    DateTime @default(now())
1711  updatedAt    DateTime @updatedAt
1712 
1713  page         Page     @relation(fields: [pageId], references: [id], onDelete: Cascade)
1714 
1715  @@map("whiteboards")
1716}
1717 
1718model Review {
1719  id           String    @id @default(cuid())
1720  pageId       String
1721  state        String    // mirrors page.reviewState while open (denormalized for queries)
1722  reviewerIds  String[]  @default([]) // stable user ids (A-003: explicit lists, not roles)
1723  requestedBy  String
1724  comment      String?
1725  deadline     DateTime?
1726  decision     String?   // approved | changes_requested (set on close)
1727  decidedAt    DateTime?
1728  decidedBy    String?
1729  openedAt     DateTime  @default(now())
1730  closedAt     DateTime?
1731 
1732  page         Page      @relation(fields: [pageId], references: [id], onDelete: Cascade)
1733 
1734  @@index([pageId, state])
1735  @@index([reviewerIds])
1736  @@map("reviews")
1737}
1738 
1739model Task {
1740  id           String    @id @default(cuid())
1741  workspaceId  String
1742  pageId       String    @unique // 1:1 with pages.kind='task' (A-006): page carries blocks/comments/permissions
1743  title        String
1744  assigneeIds  String[]  @default([]) // stable user ids
1745  createdBy    String
1746  status       String    @default("todo") // todo | in_progress | done | cancelled
1747  dueAt        DateTime?
1748  createdAt    DateTime  @default(now())
1749  updatedAt    DateTime  @updatedAt
1750 
1751  page         Page      @relation(fields: [pageId], references: [id], onDelete: Cascade)
1752 
1753  @@index([workspaceId, status])
1754  @@index([assigneeIds])
1755  @@index([workspaceId, dueAt])
1756  @@map("tasks")
1757}
1758 
1759model PageWatch {
1760  id        String   @id @default(cuid())
1761  pageId    String
1762  userId    String
1763  createdAt DateTime @default(now())
1764 
1765  page      Page     @relation(fields: [pageId], references: [id], onDelete: Cascade)
1766  user      User     @relation(fields: [userId], references: [id], onDelete: Cascade)
1767 
1768  @@unique([pageId, userId])
1769  @@index([userId])
1770  @@map("page_watches")
1771}
1772 
1773model UserRecentItem {
1774  id       String   @id @default(cuid())
1775  userId   String
1776  itemType String   // "page" | "database" | "whiteboard" | "task"
1777  refId    String
1778  at       DateTime @default(now())
1779 
1780  user     User     @relation(fields: [userId], references: [id], onDelete: Cascade)
1781 
1782  @@unique([userId, itemType, refId])
1783  @@index([userId, at])
1784  @@map("user_recent_items")
1785}
1786 
1787model SavedSearch {
1788  id          String   @id @default(cuid())
1789  userId      String
1790  workspaceId String?  // null = cross-workspace (member-scoped at query time)
1791  name        String
1792  queryJson   Json     // the search filter/sort query (same shape as POST /search body)
1793  createdAt   DateTime @default(now())
1794  updatedAt   DateTime @updatedAt
1795 
1796  user        User     @relation(fields: [userId], references: [id], onDelete: Cascade)
1797 
1798  @@index([userId])
1799  @@map("saved_searches")
1800}
1801