6.3 KiB
Database schema
This document defines the intended relational shape. EF Core migrations are the executable source once implemented. Names may be refined in M0/M1 through an ADR, but constraints and invariants must remain.
Conventions
- PostgreSQL
uuidprimary keys generated by the application or database. timestamptzfor instants.- UTC application semantics.
- lowercase snake_case database names.
- explicit foreign keys and delete behavior.
citextor normalized columns for case-insensitive unique values.jsonbonly for versioned, non-core metadata and import diagnostics.- integer/bigint monotonic revision numbers where scoped.
- byte/string concurrency token exposed as ETag.
- no database credentials or secrets in data migrations.
Identity and workspace
installation
idsingleton keyinstance_iduuid uniqueproduct_versionschema_versionprimary_localetime_zonesetup_completed_at- timestamps / concurrency version
users
Use ASP.NET Core Identity-compatible tables with application extensions:
- display name
- enabled state
- last login
- security stamp
- created/updated timestamps
workspaces
idname- normalized
slug - settings version
- timestamps
- archived timestamp
Unique: normalized slug.
workspace_memberships
- workspace/user composite uniqueness
- role
- timestamps
Constraint: valid role enum. Application invariant: final admin cannot be removed.
Knowledge tables
pages
Important columns:
id uuid primary key
workspace_id uuid not null
parent_page_id uuid null
sort_key text not null
title varchar(300) not null
normalized_title varchar(300) not null
slug varchar(320) not null
markdown text not null
document_type smallint not null
document_status smallint not null
current_revision bigint not null
content_hash char(64) not null
concurrency_version bigint not null
created_by / updated_by uuid
created_at / updated_at / published_at timestamptz
trashed_at / purge_after timestamptz null
search_vector tsvector generated or maintained
Indexes:
(workspace_id, parent_page_id, sort_key, id)(workspace_id, slug)(workspace_id, updated_at desc, id)- partial active-page indexes where
trashed_at is null - GIN
search_vector - GIN/GiST trigram on normalized title
- optional path/ancestor closure only if measured need justifies it
Cycle prevention is primarily application-controlled and integration-tested. A recursive query validates moves transactionally.
page_revisions
Unique (page_id, revision_number). Store immutable snapshots and content hash.
Indexes:
- page/revision descending
- workspace/created timestamp
- author/time where audit UI needs it
No cascade delete during ordinary content lifecycle.
drafts
Unique (workspace_id, user_id, draft_key). Draft content is private to the author.
page_links
Unique identity includes source page, occurrence key and source revision. Index target page for backlinks and unresolved text for maintenance.
page_relations
Unique (source_page_id, target_page_id, relation_type) unless multiple labels are
explicitly supported.
tags / page_tags
- normalized tag unique per workspace
- composite join primary key
- indexes for tag-filter search
favorites / recent_visits
Composite user/content uniqueness. Recent visits use upserted last-view time and bounded retention.
Attachments
attachment_blobs
Primary key is 64-character lowercase SHA-256.
Columns include:
- length
- detected media type
- storage/preview/integrity states
- created/verified timestamps
- reference count cache only if transactionally maintained
attachment_references
Stable logical ID, workspace, blob hash, safe original filename and descriptive metadata.
attachment_usages
Polymorphic ownership must not use an unconstrained arbitrary string alone. Prefer separate nullable constrained columns or typed join tables for page/project update usage. The selected design requires an ADR.
Indexes support owner lookup and orphan discovery.
Projects
projects
Unique normalized project slug per workspace. Index lifecycle status and last update.
project_documents
Unique (project_id, page_id, role).
project_updates
Ordered by (occurred_at desc, id desc). Immutable content with correction/supersession
links.
project_milestones
Unique name or sequence per project; stable sort key.
technology_tags / project_technology_tags
Normalized unique technology name per workspace.
Decisions and runbooks
decision_records
- unique page profile
- unique accepted decision number per project
- self-referencing supersession constraints
- indexes by project/status/date
runbook_profiles
- unique page profile
- risk/validation metadata
- review/validation indexes
Imports
import_jobs
Unique source fingerprint plus invocation identity does not automatically prevent a new deliberate import, but idempotency keys prevent duplicate execution.
import_items
Unique (import_job_id, source_identity). Index target identities and status.
import_provenance
Links imported target rows to job/item. This table is the authoritative rollback scope.
Audit, jobs and outbox
audit_events
Append-only, indexed by workspace/time, actor/time, target/time and correlation ID. Partitioning is deferred until measured volume warrants it.
background_jobs
Durable state, lease owner/expiry, attempt count, schedule, progress and safe diagnostics.
outbox_messages
Index unprocessed (available_at, id). Retention after successful dispatch is
configurable.
Search implementation
Use the PostgreSQL simple text-search configuration initially because documents mix
Dutch, English, code, commands and product names. Maintain normalized searchable text
from:
- title;
- stripped Markdown text;
- tags;
- selected structured metadata.
Use trigram matching for titles/slugs/tags and explicit ranking weights. Permission filters must be part of the query, not a post-filter after result selection.
Migration requirements
Every migration must:
- be deterministic;
- be tested from the previous released schema;
- avoid silent data truncation;
- include data backfill verification where needed;
- document locks/expected duration for large tables;
- have a tested application rollback strategy even when SQL downgrade is unsafe.