Versioned disclosure documents · DAG-based approvals · append-only audit trail · QA gating
Monorepo: Next.js 16 (App Router, Server Actions) + PostgreSQL + Drizzle. Models fund-level documents, revision history, a parallel review workflow (DAG with React Flow), a Filing QA workspace (checklist, redlines, iXBRL drafts, HTML export stub), and an append-only audit_events log. Business rules for submission and sign-off are enforced on the server (see below)—not button-only UX.
Problem. Fund and ETF operators need a repeatable operating model for disclosure content: who may edit, who must review, what evidence is required before a version is “ready,” and how leadership proves what happened months later (exams, internal audit, or litigation support).
What PortCheck demonstrates.
| Business outcome | How the app supports it |
|---|---|
| Policy-aligned process | Required checklist items, evidence notes, and status gates tied to document versions — not ad-hoc email threads. |
| Segregation of duties (concept) | Demo roles (viewer / reviewer / admin) model read vs prepare vs sign-off; the same checks are re-applied on the server so the UI cannot bypass them. |
| Parallel workstreams | DAG workflow runs represent concurrent legal / risk / product tracks that must converge before final approval. |
| Defensible history | Append-only audit_events with optional integrity chaining (hash per record) to narrate tamper-evidence (demo scope — not a certified control). |
| Exam readiness (story) | Searchable audit log + version lineage (parent_version_id) support the “show the trail” conversation with compliance and internal audit. |
Personas (typical conversation). Product / disclosure owner maintains drafts and runs QA; reviewer clears checklist and advances workflow steps; admin performs formal approve with attestation; second line / audit consumes the log and integrity checks.
Scope honesty. Seed policies and export stubs are not submission-grade. The value here is the control design and server-side enforcement, not a filing vendor replacement.
| Control | Where it’s enforced |
|---|---|
| Required checklist before Submit for review | submitVersionForApproval in compliance-workspace.ts — rejects if any open required rows. |
System validation (automatic) after checklist — min length, structure, slug-specific fee % sanity, optional PORTCHECK_DEMO_BPS |
evaluateSystemValidation in apps/web/lib/validation/system-validation.ts; enforced in submitVersionForApproval, approveDocumentVersion, and workflow assertFinalApprovalQaGates. |
Required checklist + in_review + system validation before completing workflow final approval |
workflow.ts — assertFinalApprovalQaGates on step transition to completed. |
| Evidence note length when completing required checklist items | toggleChecklistItem — minimum length for audit trail. |
Formal document approve (in_review → approved) |
approveDocumentVersion — admin role; requires closed required QA + completed workflow final (if a run exists); attestation text; version_approved audit row. |
| Reject / reopen | rejectDocumentVersion / reopenRejectedVersion — rationale + audit. |
Role model is a cookie (viewer / reviewer / admin) for the demo, but checks are duplicated server-side on every mutation.
- Funds & documents — Legal entity / product structure; paginated document versions with lifecycle states (
draft/in_review/approved/rejected); optional parent or previous-revision redlines for change control storytelling. - Workflow runs — Run bound to a document version;
step_executions+ React Flow; DAG rules inworkflow-rules-engine.tsandactions/workflow.ts— models parallel approvals and join points. - Audit trail —
/audit— paginated, filterableaudit_events(actor, action, entity, payload) for operational and second-line review. - Filing QA workspace — Per-version: content, redline, grouped checklist, iXBRL drafts (validator), export stub — readiness before external filing channels.
- Compliance hub — Policy library + role switcher (stand-in for IAM / entitlements).
- Review queue — Operational triage:
draft/in_review/rejectedwith open required-item counts.
- Next.js 16 (App Router), React 19, Server Actions
- PostgreSQL + Drizzle ORM (
packages/dbworkspace package) - Turborepo, TypeScript, ESLint
- React Flow (
@xyflow/react), diff (redlines)
- Node.js (see repo/tooling; Next 16 compatible)
- PostgreSQL and a
DATABASE_URLconnection string
From this monorepo root (where root package.json lives):
-
Environment — Set
DATABASE_URLin.envat the repo root (or your shell):DATABASE_URL=postgres://user:password@localhost:5432/your_db
-
Install & migrate & seed:
npm install cd packages/db npm run db:migrate npm run db:seed cd ../..
-
Run the web app:
npm run dev:web
Or:
npx turbo dev --filter=web -
Open http://localhost:3000.
| Command | Description |
|---|---|
npm run dev:web |
Dev server for apps/web (see root package.json) |
npm run build |
Often run per app, e.g. cd apps/web && npm run build |
npm test |
Vitest — apps/web/tests/*.test.ts (DAG rules, system validation) + packages/db/tests/*.test.ts (audit hash) |
cd apps/web && npm run test:watch |
Re-run web tests on change |
cd packages/db && npm run db:studio |
Drizzle Studio (inspect DB) |
Automated checks focus on pure, security-relevant logic without a live browser or Postgres:
validateWorkflowTransition— illegal status changes, predecessor gating frompending/blocked, terminal states.computeAuditRecordHash— deterministic SHA-256, chain linkage to previous record hash, stable payload key ordering.evaluateSystemValidation— document length / structure, fee-line percentage band (demo), optional demo BPS line (apps/web/tests/system-validation.test.ts).
Server Actions and DB transactions are still best extended with integration tests (Postgres test container or DATABASE_URL to a throwaway DB) when you want end-to-end gate coverage.
apps/web— Next.js UI and server actionspackages/db— Drizzle schema, migrations, seed scriptapps/docs— Stub docs app (original turbo template; optional)
Key app paths:
apps/web/app/actions/workflow.ts— Step updates, auto wavesapps/web/lib/workflow-rules-engine.ts— DAG transition rulesapps/web/lib/demo-role-server.ts/demo-role-constants.ts— Demo RBACpackages/db/src/schema/— Drizzle tables (core,workflow,compliance)
This section is the single place to answer: “Where is it OK to put logic?” The stack is intentionally thin (no separate domain package yet); these rules keep the demo maintainable and safe.
flowchart TB
subgraph browser ["Browser"]
C["Client components\n(*-client.tsx)"]
end
subgraph next ["Next.js server"]
P["Server Components\n(app/**/page.tsx, layout)"]
A["Server Actions\n(app/actions/*.ts)"]
R["Route handlers\n(app/api/**/route.ts)"]
L["Shared libs\n(apps/web/lib/*.ts)"]
end
subgraph pkg ["Workspace package"]
DB["@repo/db\nschema · migrations · seed · db client"]
end
PG[(PostgreSQL)]
C -->|"calls"| A
C -->|"GET /api/…"| R
P -->|"read model"| DB
A --> L
A --> DB
R --> L
R --> DB
DB --> PG
Data flow shorthand: UI triggers Server Actions or route handlers for anything that must be trusted; RSC pages may query @repo/db directly for reads. Append-only audit and state mutations always go through server-side code, never from the client alone.
| Kind of logic | Put it here | Notes |
|---|---|---|
| Presentation (tabs, optimistic UI, formatting) | *-client.tsx, small helpers next to UI |
No security or authority decisions. |
| Demo RBAC (“can this cookie role do X?”) | lib/demo-role-constants.ts |
Capability map; enforce again in Server Actions / routes. |
| Resolve current demo role | lib/demo-role-server.ts |
Server-only; reads cookie. |
| DAG step transition rules (pure) | lib/workflow-rules-engine.ts |
No I/O. Same rules used by UI previews and actions/workflow.ts. |
| QA / sign-off readiness (aggregated read model) | lib/version-approval-readiness.ts |
DB reads composed for gates; keep mutations in actions. |
| iXBRL HTML export (string build) | lib/inline-ixbrl-html.ts |
Called from route handler; no DB rules beyond supplied rows. |
| Orchestration: mutations, audits, revalidate | app/actions/workflow.ts, app/actions/compliance-workspace.ts |
Source of truth for “what happened”; call libs for pure checks. |
| Binary / attachment-style HTTP | app/api/**/route.ts |
e.g. EDGAR-style download; still check role here. |
| Table shapes, migrations, seed | packages/db |
Schema + data definition; avoid embedding business policy in column defaults beyond obvious constraints. |
| Direct DB reads in pages | page.tsx (server) |
Fine for lists/detail when no shared helper exists yet; prefer extracting repeated queries into lib/ or a future packages/domain. |
- No standalone BFF or public REST/GraphQL API layer — Next Actions + a few routes are the API.
- No event bus or outbox — audit rows approximate compliance logging.
- Production IAM (SSO, ABAC, delegations) is not modeled; the cookie is a stand-in.
When the project grows, the first structural deepening step is usually: extract pure policy from actions into packages/domain (or lib/domain) without changing behavior — this README’s table then gains an extra column for that layer.
Seed policies, HTML/iXBRL export stubs, and validation rules are not submission-grade. Real filings require certified processes, correct taxonomies, and firm-specific controls.
This repo uses Turborepo. To build all packages:
npx turbo buildSee Turborepo docs for caching, filters, and remote cache.