Skip to content

isabelccc/PortCheck

Repository files navigation

PortCheck — disclosure control plane (reference build)

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.


Business narrative (why this exists)

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.


Server-enforced approval & QA gates (control matrix → code)

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.tsassertFinalApprovalQaGates on step transition to completed.
Evidence note length when completing required checklist items toggleChecklistItem — minimum length for audit trail.
Formal document approve (in_reviewapproved) approveDocumentVersionadmin 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.


Features (product map)

  • 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 in workflow-rules-engine.ts and actions/workflow.ts — models parallel approvals and join points.
  • Audit trail/audit — paginated, filterable audit_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 / rejected with open required-item counts.

Stack

  • Next.js 16 (App Router), React 19, Server Actions
  • PostgreSQL + Drizzle ORM (packages/db workspace package)
  • Turborepo, TypeScript, ESLint
  • React Flow (@xyflow/react), diff (redlines)

Prerequisites

  • Node.js (see repo/tooling; Next 16 compatible)
  • PostgreSQL and a DATABASE_URL connection string

Setup

From this monorepo root (where root package.json lives):

  1. Environment — Set DATABASE_URL in .env at the repo root (or your shell):

    DATABASE_URL=postgres://user:password@localhost:5432/your_db
  2. Install & migrate & seed:

    npm install
    cd packages/db
    npm run db:migrate
    npm run db:seed
    cd ../..
  3. Run the web app:

    npm run dev:web

    Or: npx turbo dev --filter=web

  4. Open http://localhost:3000.

Useful commands

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 Vitestapps/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)

Testing (invariants)

Automated checks focus on pure, security-relevant logic without a live browser or Postgres:

  • validateWorkflowTransition — illegal status changes, predecessor gating from pending / 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.

Repository layout

  • apps/web — Next.js UI and server actions
  • packages/db — Drizzle schema, migrations, seed script
  • apps/docs — Stub docs app (original turbo template; optional)

Key app paths:

  • apps/web/app/actions/workflow.ts — Step updates, auto waves
  • apps/web/lib/workflow-rules-engine.ts — DAG transition rules
  • apps/web/lib/demo-role-server.ts / demo-role-constants.ts — Demo RBAC
  • packages/db/src/schema/ — Drizzle tables (core, workflow, compliance)

Architecture boundaries

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.

System diagram

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
Loading

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.

Where logic is allowed

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.

Explicit non-goals (still shallow on purpose)

  • 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.

Disclaimer

Seed policies, HTML/iXBRL export stubs, and validation rules are not submission-grade. Real filings require certified processes, correct taxonomies, and firm-specific controls.

Turborepo

This repo uses Turborepo. To build all packages:

npx turbo build

See Turborepo docs for caching, filters, and remote cache.

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages