LegalPresence is a litigation-support platform: a public site, an authenticated CRM + AI legal workspace, an encrypted secrets vault, and an OTOC-gated reasoning engine over a case system of record. This document describes the revised three-tier architecture and the routing that implements it.
legalpresence.com · www.legalpresence.com — the marketing / product site (static) and the /api/signup capture endpoint. Currently gated behind HTTP Basic auth (realm "LegalPresence — Restricted", via Cloudflare) as a pre-launch private site; open it publicly at launch. CDN-friendly; WAF/rate-limit at the edge.
crm.legalpresence.com — the Twenty CRM (system of record) and the Legal Studio (AI evidence workspace, RAG, document viewer with ontology concept markup). Auth-gated. This is where the legal work happens.
SecureVault (credentials), PostgreSQL ×3, Redis, NATS messaging, and the reasoning engine. Not publicly routed — reachable only from the application tier. Reasoning outputs land in a staging schema and promote to the CRM only on counsel approval.
| Hostname | Path | Backend | Role |
|---|---|---|---|
| legalpresence.com | / | static /var/www/html | Marketing site |
| legalpresence.com | /api/signup | :8462 | Signup capture |
| crm.legalpresence.com | /graphql /metadata /rest /auth /files | Twenty CRM :3360 | System of record |
| crm.legalpresence.com | /studio /ai /query | Legal Studio :3361 | AI evidence workspace |
| (internal) | vault API / admin | SecureVault :8480 / :3380 | Secrets — internal only |
flowchart TB V(["Visitors"]):::actor --> PUB["PUBLIC TIER — legalpresence.com
static site · /api/signup · pre-launch gated"]:::pub C(["Counsel"]):::actor --> CRM subgraph APPT["APPLICATION TIER · crm.legalpresence.com"] direction LR CRM["Twenty CRM :3360
system of record"]:::app STU["Legal Studio :3361
RAG · concept markup"]:::app end subgraph INF["INFRASTRUCTURE TIER · internal only"] direction LR VLT["SecureVault"]:::infra DBS[("Postgres ×3 · Redis")]:::infra NTS["NATS"]:::infra RSN["Reasoning engine"]:::infra STG["lp_reasoning_staging
PENDING"]:::infra end STU --> CRM CRM --> DBS STU --> VLT RSN --> STG STG -->|promote on counsel approval| CRM APPT --> INF classDef actor fill:#eef1f6,stroke:#5c6577,color:#141b2e; classDef pub fill:#e7f2ec,stroke:#2e7d5b,color:#141b2e; classDef app fill:#e8eefa,stroke:#1f3a5f,color:#141b2e; classDef infra fill:#fbecef,stroke:#b23a3a,color:#141b2e;
The tiers above show where things run. This shows how one fact moves through the stack — from a marked-up sentence in an evidence document to a stability-gated reasoning object the rules engine can score. Every layer is live on lp1; the snippets below are real (trimmed) — the same worked example end to end.
flowchart LR DOC["① Document markup
Legal Studio concept tags"]:::app CRM2["② CRM record
Twenty — system of record"]:::app SKOS["③ SKOS vocabulary
controlled terms + crosswalk"]:::infra RSN2["⑤ Reasoning object
OTOC-scored RDF assertion"]:::infra SHACL["④ SHACL shapes
pySHACL admissibility gate"]:::infra OBS["⑥ Obsidian vault
graph view for humans"]:::pub DOC --> CRM2 CRM2 -->|project_rdf · project_assertions| RSN2 SKOS -.governs vocabulary.-> CRM2 SHACL -.validates on ingest.-> RSN2 RSN2 -->|export_obsidian_full.py| OBS classDef app fill:#e8eefa,stroke:#1f3a5f,color:#141b2e; classDef infra fill:#fbecef,stroke:#b23a3a,color:#141b2e; classDef pub fill:#e7f2ec,stroke:#2e7d5b,color:#141b2e;
A sentence in an evidence document is tagged with the ontology concepts it touches, and linked to the evidence artifact and the legal element it bears on. 1,250 of 1,483 documents are concept-tagged.
<span class="lp-concept" data-evidence="id/evidence/22b1e037…" data-element="id/element/2bab1625…"> Counsel for the Plaintiffs stated their engineer and prior counsel inspected the unit and found it compliant with the Certificate of Occupancy. </span>
The tag becomes a factAssertion row in Twenty — the single source of truth. Nothing downstream is hand-maintained; it all projects from here. Born UNREVIEWED (the counsel gate) and unscored until the OTOC gate runs.
factAssertion { factText: "…inspected the unit and found it compliant with the C of O.", supportedByEvidenceRef: "id/evidence/22b1e037…", supportsElementRef: "id/element/2bab1625…", assertionStrength: "INDIRECT", assertionReviewStatus: "UNREVIEWED", // counsel gate — never auto-approved stabilityStatus: "STABLE", // written by the OTOC gate (step 5) otocScore: 0.0, llmGenerated: "YES" }
Every enum is a skos:Concept in a ConceptScheme; drifted CRM values map to the canonical concept with skos:closeMatch — non-destructive (records keep their current value; the graph gains interoperable terminology). Roles are matter-scoped: the same person is PLAINTIFF in one index and DEFENDANT in another.
lpc:PartyRoleScheme a skos:ConceptScheme . lpc:PLAINTIFF a skos:Concept ; skos:inScheme lpc:PartyRoleScheme ; skos:prefLabel "Plaintiff" ; skos:scopeNote "Trial-level action; party bringing suit." . # a drifted CRM enum value maps to the canonical concept — record untouched crm:"Claimant" skos:closeMatch lpc:PLAINTIFF . # the same party = PLAINTIFF in one index, DEFENDANT in a related action
Hard integrity shapes run by rules.py layer 1. A node that violates them cannot promote from graph:ingest to graph:reviewed. Email evidence must carry a real DKIM verdict — not left blank, not NOT_APPLICABLE.
lp:EvidenceArtifactShape a sh:NodeShape ; sh:targetClass lp:EvidenceArtifact ; sh:property [ sh:path lp:contentHash ; sh:minCount 1 ; sh:message "Content hash required for integrity + admissibility." ] ; sh:property [ sh:path lp:counselReviewStatus ; sh:minCount 1 ; sh:in ( "UNREVIEWED" "IN_REVIEW" "APPROVED" "REJECTED" "NEEDS_SUPPLEMENT" ) ] ; sh:property [ sh:path lp:dkimStatus ; sh:in ( "PASS" "FAIL" "NONE" "NOT_APPLICABLE" ) ] .
The projected RDF the rules/inference engines reason over. The OTOC gate re-extracts under K perturbations: otocScore = (K − reproduced)/K. Only STABLE assertions count toward element readiness — and only after counsel approval.
<id/assertion/00232d7c…> a lp:Assertion ; rdfs:label "…inspected the unit and found it compliant with the C of O." ; lp:supportedByEvidence <id/evidence/22b1e037…> ; lp:supportsLegalElement <id/element/2bab1625…> ; lp:hasStrength "INDIRECT" ; lp:assertionReviewStatus "UNREVIEWED" ; lp:otocScore 0.0 ; lp:stabilityStatus "STABLE" . # survived K perturbations → counts toward readiness
The gate in action: the opposing prior-sworn statement "all mechanical equipment … installed at the penthouse roof" scored otocScore 1.0 → QUARANTINED (perturbation-fragile) and is excluded from readiness. Same pipeline, opposite verdict — the hallucination filter doing its job, auditable rather than silent.
What ties the layers together: the CRM is the source; project_rdf.py / project_assertions.py emit the RDF; shapes.ttl (pySHACL) is the hard admissibility gate; vocabularies.ttl (SKOS) is the shared, matter-scoped vocabulary the CRM enums crosswalk to; otoc_score.py stamps stability; rules.py scores element readiness counting only STABLE + counsel-approved assertions. Reasoning stays a one-way derived layer — it never writes back to the record except through the counsel-gated staging promotion.
export_obsidian_full.py regenerates an Obsidian vault from the live RDF graph — one markdown note per Matter / EvidenceArtifact / Assertion / LegalElement / StrategyCandidate, with every relation rendered as an Obsidian [[wikilink]]. Obsidian's built-in graph view then becomes the mapper — no bespoke graph-rendering UI to build or maintain. It is a pure regeneration from the graph (never hand-edited; overwritten on every run), so it can't drift from the system of record.
| Layer | Does Obsidian add value? |
|---|---|
| Metadata / instances | YES Browse parties, evidence and assertions as linked notes; the graph view surfaces clusters and orphans a table can't. Effectively free — the exporter already exists. |
| SKOS vocabulary | YES skos:broader/narrower/related become graph edges, so a concept scheme turns into a navigable map — and it's a genuinely good surface for authoring the vocabulary as linked notes before it's promoted to vocabularies.ttl. |
| SHACL validation | NO (as a gate) Obsidian does not enforce shapes — pySHACL does. It can display a violation if we write it into the note, but it is a viewer, never the validator. |
| System of record / runtime | NO It is a regenerated, read-mostly human surface. The CRM stays the source of truth; the RDF + SHACL + OTOC pipeline stays the runtime. |
Verdict: keep Obsidian as a generated read / explore / author layer over the same RDF — high value for humans navigating the metadata and the SKOS concept schemes, near-zero cost since it's a pure export. It does not replace SHACL (validation) or the CRM (storage); treat any hand-edit to the vault as disposable, because the next regeneration overwrites it.
Three real views, generated from the current graph (1,646 notes: 886 assertions · 725 evidence · 25 elements · 5 matters · 4 strategies). The graph below is the Factor-2 (Irreparable Harm) cluster; the note is the actual element note; both are pure regenerations from the RDF.
LegalPresence-Obsidian-Vault/ ├── Litigation Map.md # index — every matter, element, strategy ├── Matters/ (5) ├── Legal Elements/ (25) ├── Evidence/ (725) ├── Assertions/ (886) └── Strategies/ (4)
Regenerate with export_obsidian_full.py after any batch; open the folder as a vault and Obsidian's own graph view renders the above live — pan from an element to its evidence to its assertions in one canvas. Same QUARANTINED nodes the rules engine excludes are visible (greyed) rather than hidden.
The rules engine (rules.py + inference_engine.py + shapes.ttl) is the non-probabilistic core. The LLM handles detection, extraction and natural-language framing; the rules engine alone does anything that carries legal weight. The contract is "LLM proposes, rules dispose" — nothing touching admissibility, readiness, motion viability, or a court filing is decided by model output. OTOC sits between them as the stability filter.
| Role | What it does | Where |
|---|---|---|
| Gate | SHACL admissibility shapes — content-hash, date-provenance, DKIM verdict, counsel-review status must be present, or the node can't promote ingest → reviewed. | shapes.ttl · pySHACL · layer 1 |
| Derive | Deterministic entailments — authentication (DKIM/court-record), party attribution + provenance tier, authored-by edges. Never asked of the model. | inference_engine.py |
| Score | Element readiness — only STABLE + counsel-approved assertions add weight; gated-out items surfaced, never silently dropped. | rules.py · layer 3 |
| Conclude | Per-motion viability as an elements-test (below), with contradiction discount + defensive exposure. | motion_viability.py |
Each motion is encoded as its legal test, not a soft weight sum: OR motions succeed on any one sufficient ground (PI-vacatur — a negated W.T. Grant factor or CPLR 5015(a)(3) fraud); AND motions need every element (contempt — order & knowledge & mandate & violation & prejudice & proof). Two new pieces feed it: project_contradictions.py projects the contradiction records into RDF (lp:contradictsAssertion / lp:impeachedBy), and rules.py discounts any approved support impeached by a STABLE+approved contradiction — so an argument can't rest on a fact the other side can destroy. The engine reports, per motion: bringable-now / potential / gaps / defensive exposure / next best move.
MOTION VIABILITY — deterministic elements-test nothing counsel-approved yet → "bringable NOW" is False everywhere by design ### PI_VACATUR [OR-test · CPLR 6314 / 5015(a)(3)] 🟡 POTENTIAL Any ONE sufficient ground (a negated W.T. Grant factor OR 5015(a)(3) fraud) supports modify/vacate. elements: 0 approved · 4 candidate · 4 gap (of 8) GAPS (obtain STABLE evidence): ✗ W.T. Grant Factor 4 — Public Interest ✗ CPLR 6312(b) — Undertaking / bond defect ✗ CPLR 6314 — Lift-condition DEFENSIVE EXPOSURE (STABLE contradictions hitting our own support): ⚠ 1× DIRECT · risk=UNCLEAN_HANDS ⚠ 1× IMPEACHMENT · risk=ESTOPPEL ⚠ 9× DIRECT · risk=CREDIBILITY NEXT: APPROVE the strongest ground « CPLR 5015(a)(3) — Fraud » (30 candidates, w3) ### CONTEMPT [AND-test · Judiciary Law §753] 🟡 POTENTIAL Conjunctive — ALL six elements required. 6 candidate · 0 gap → fully satisfiable on approval. ⚠ 2 supporting assertion(s) impeached by a STABLE contradiction (candidate exposure) NEXT: APPROVE 23 candidate(s) for « Violation of the Mandate »
The boundary: the engine gives the evidence-readiness substrate — which motion is bringable, on what, with what gaps and what exposure — and flags when a plan rests on impeached or inadmissible support. It does not make the strategic calls (which vehicle to lead with, fight vs. settle, tone, sequencing to the judge). Those stay with counsel; capability is not authority.
One level above motion viability sits the theory layer (theory_engine.py): affirmative legal theories — credibility impeachment, changed-position estoppel, unclean hands as sword, bad-faith injunction posture, undertaking defect, changed-circumstances vacatur — each scored on a transparent composite of grounding & stability (share of OTOC-STABLE support), element coverage (how much of the legal test has evidence), declared leverage (strategic payoff), and inverted risk (structural penalties like reargument exposure, plus impeached-support fraction). A stable contradiction is no longer a flag: it is routed by declared selectors into the theories it arms, deduplicated on the assertion pair so a twice-detected inconsistency counts once.
Nothing is hardcoded. The theory registry, contradiction selectors, leverage multipliers, risk penalties, risk narratives and the composite weights themselves are all declarative data in theories.ttl — the engine is a generic evaluator. A new theory, a recalibrated weight, or an entirely new case’s theory set is an ontology edit, never a code change. This is also the learning hook: closed cases with known outcomes can promote de-identified theory-outcome observations through the cross-case privilege firewall, and the calibrated priors return as counsel-reviewed edits to the same data file.
lp_reasoning_staging as PENDING; only counsel-approved proposals promote.| Priority | Item |
|---|---|
| DONE | Vault is IP-allowlisted (deny all except localhost + admin IPs) — no public access. Add remaining admin IPs (co-counsel / client / Berlin) when known. |
| BY DESIGN | Site is intentionally restricted (HTTP Basic auth, realm "LegalPresence — Restricted") — a private pre-launch gate. Remove the gate when launching publicly. |
| SOON | Resolve multi-tenancy: commit to flat <case>.legalpresence.com (with the login-config fix) or remove the dormant wildcard and run single-workspace. |
| SOON | Split databases + vault to a second host — lp1 is currently a single point of failure for a live matter. |
| DONE | Reasoning-engine staging boundary (lp_reasoning_staging + review/promote) live. |
Honest self-assessment against a third-party audit (technical · evidentiary · security · governance). The design is sound as an investigative / work-product engine; the gaps below are what a litigation-grade reasoning engine additionally requires before any court output depends on it. Severity: P0 audit-blocking · P1 must-fix · P2 hardening.
| ID | Sev | Finding | Remediation | Owner | Status |
|---|---|---|---|---|---|
| A-01 | P0 | No measured error rate (Daubert/Frye). No eval harness — validate.py checks SHACL structure only; extraction, OTOC and the contradiction detector have no precision/recall or false-positive rate, and the contradiction detector is known to emit false positives. |
Build a gold-standard labelled sample; measure precision/recall/FP per component; document the method. Keep AI strictly investigative work-product, never the evidence itself. | Eng + Counsel | PARTIAL |
| A-02 | P0 | Audit log not tamper-evident. decisions.ttl is a flat append with no hash-chain, signing, or external anchor. Per-file SHA-256 seals cover evidence content, not the decision trail — alterations to the reasoning record are undetectable. |
Hash-chain DecisionRecords (prevHash), sign, RFC-3161 timestamp, anchor a daily Merkle root. |
Eng | PARTIAL |
| A-03 | P0 | Single point of failure + unverified backup of the system of record. CRM DB, vault DB, rules DB, vault API, engine and site all on one host; only ad-hoc manual DB dumps exist and the DR job pushes source, not Postgres data. Untested backup = no backup. | Scheduled pg_dump of the CRM + vault DBs offsite; a tested restore drill; a second host / HA. |
Security/Ops | PARTIAL |
| B-01 | P1 | OTOC measures reproducibility, not truth. Token-overlap stability (no embeddings); grounding is a shallow quote-in-OCR match. A STABLE assertion can still misread its source. | Add a citation-faithfulness / entailment verifier that the cited passage actually supports the assertion. | Eng | PARTIAL |
| B-02 | P1 | Authentication tiered only for email. Non-email evidence (chat exports, PDFs, scans) has no authentication-basis or custodian-affidavit linkage; the court-ready rule recognises only DKIM / court-record. | Add a general authenticationBasis field + custodian-affidavit evidence type; extend the court-ready gate. |
Eng + Counsel | PARTIAL |
| B-03 | P1 | No privilege / work-product segregation or litigation hold. The graph holds work product and potential admissions against interest (the engine labels own-side statements QUARANTINED / flags unclean-hands) — discoverable and damaging if not privileged. | Document a privilege + discoverability strategy; mark reasoning artifacts work-product; access-control; counsel sign-off; litigation hold. | Counsel | OPEN |
| B-04 | P1 | No per-record model / prompt pinning. Extraction model is an env var; the exact model version + prompt hash aren't stamped per assertion, so an extraction can't be deterministically replayed or explained at trial. | Stamp model-id + prompt-hash + params + timestamp on every assertion and decision record. | Eng | PARTIAL · BUILT fwd |
| B-05 | P1 | Prompt-injection exposure. Opposing-party-produced documents are fed straight into extraction prompts; a crafted document could steer output. No documented input isolation. | Treat document text as untrusted (delimit/sandbox); add injection regression tests. | Eng | OPEN |
| B-06 | P1 | Hand-assigned element weights. Readiness % and motion viability depend on subjective weights with no rationale or sensitivity analysis. | Document weight rationale; run sensitivity analysis; ideally derive from legal-authority mapping. | Eng + Counsel | OPEN |
| B-07 | P1 | PII / cross-border data protection. Party PII, medical mentions and settlement communications sit in the graph and the exported vault; an EU party implicates GDPR. No retention/deletion/DPA posture; at-rest DB encryption + key rotation undocumented. | Data-protection policy (retention, deletion, GDPR basis); encrypt DBs at rest; document KMS + rotation; minimise PII in the Obsidian export. | Security/Ops + Counsel | OPEN |
| C-01 | P2 | Corpus completeness not quantified. Reasons over a partial, mostly-unreviewed corpus with no blind-spot metric. | Coverage metric + explicit accounting of the un-reasoned set. | Eng | OPEN |
| C-02 | P2 | Approval not role-restricted / non-repudiable. APPROVED is an unrestricted status; not proven to be gated to a licensed-attorney account or immutably attributed (who/when). |
RBAC restricting approval to attorney accounts; immutable approval log. | Eng + Counsel | OPEN |
| C-03 | P2 | No court-presentable provenance report. DecisionRecords are machine JSON; no plain-English "why is this counted / where did it come from" output for a judge or opposing counsel. | Human-readable provenance report generator per assertion / bundle. | Eng | OPEN |
| C-04 | P2 | OTOC thresholds unvalidated. TAU_LOW/TAU_HIGH/K chosen, not validated for sensitivity. |
Sensitivity analysis over thresholds against the gold set (depends on A-01). | Eng | PLANNED |
| C-05 | P2 | Extractors are partisan by construction. Prompts mine one side's facts (legitimate advocacy) — must not be represented to an auditor as neutral fact-finding. | Label extraction outputs as advocacy work-product in the schema and any audit representation. | Eng + Counsel | OPEN |
| C-06 | P2 | No separation of duties. Builder and validator are the same; ADRs exist but no independent review / SDLC gate. | Independent validation + code review before reliance; document the SDLC. | Eng + Counsel | OPEN |
Mitigating controls already in place (credit at audit): counsel gate + court-ready rule, OTOC hallucination filter with visible quarantine, single-source projection + CI drift-check, SHACL admissibility gate, DKIM provenance tiering, per-file SHA-256 content seal, append-only decision records, no-mail-without-approval, and party-name anonymization on all public documentation. The register is scored against those — the P0s are the audit-blocking remainder.
seal_decisions.py hash-chains the 71 decision records into a SHA-256 chain (root e39fe49e…41b0c5); verify_ledger.py recomputes and confirms INTACT, and an injected record was correctly detected as DIVERGENCE. Remaining: RFC-3161 external anchor of the root.lp-db-backup.sh (daily cron) dumps all three DBs (CRM 150 MB, vault 16 MB, rules) with 14-day retention; the restore drill verified 126/126 tables restored into a scratch DB. Remaining: offsite/immutable copy + second host.eval_harness.py measured automated proxies: citation-groundedness 209/209 (100%) of STABLE assertions have their quote located in source OCR; contradiction exact-duplicate FP 0/145 (0%). Remaining: a human-labelled gold set for Daubert-grade precision/recall (proxies only).factAssertion (model · prompt-version hash · extractor version · source hash · gen-params); provmanifest.py wired into all three extractors (forward-path proven live), 913/913 backfilled (98.5% carry a real source hash), and projected into RDF (lp:modelId … × 913). Remaining: legacy model/prompt are unrecoverable (honestly marked pre-manifest); full manifest is forward-going.| Standard | Relevance |
|---|---|
| FRE 702 (reliable methods; proponent shows admissibility "more likely than not") | A-01 measured error rate; C-04 threshold validation |
| FRE 901 (authentication — item is what it's claimed to be) | B-02 authentication basis; admissibility tiering |
| RFC 3161 (trusted timestamp protocol) | A-02 external anchoring of the decision ledger |
| W3C PROV-O (entity/activity/agent provenance) | B-04 per-record reproducibility manifest |
| OWASP LLM01 (prompt injection) | B-05 hostile-document input isolation |
| NIST SP 800-92 / 800-53 (log management; audit & accountability controls) | C-02 immutable approval/access logs |
Assertion lifecycle states (strict): CANDIDATE → REVIEWED → COUNSEL_APPROVED → ADMISSIBLE, plus terminal IMPEACHED / EXCLUDED. Only COUNSEL_APPROVED+ADMISSIBLE, non-IMPEACHED support may feed a motion score.
| Admissibility tier | Meaning |
|---|---|
| A1 | Authenticated original / certified record / admissible business record |
| A2 | Strong source, authentication pending |
| B1 | Email/message with metadata; DKIM/header/custodian proof pending |
| B2 | Screenshot / copied text, weak authentication |
| C | Investigative lead only — not filing-ready |
| X | Privileged, excluded, impeached or unreliable |
| Motion label | Strict meaning |
|---|---|
| NOT_READY | One or more mandatory elements has no approved support |
| POTENTIAL | Elements have candidate support, but not enough approved / authenticated / admissible |
| BRINGABLE_NOW | Every required element has attorney-approved, authenticated, admissible, non-impeached support |
| HIGH_RISK_BRINGABLE | Legally supportable but exposes major counterattack (unclean hands / estoppel / waiver / credibility / sanctions) |
"An attorney-supervised litigation-intelligence platform. It does not autonomously determine legal truth or make filing decisions. It extracts candidate assertions, maps them to legal elements, detects possible contradictions, identifies evidentiary gaps, and generates motion-readiness diagnostics. Court-facing reliance requires human counsel approval, authenticated source evidence, admissibility review, and immutable provenance." — safer than "AI legal reasoning engine."
Decision: stay on Twenty for the proof-of-concept. Supabase is the leading candidate for the eventual product backend; re-evaluate at the PoC→product transition, and never migrate the system of record while a motion is in flight.
Context. LegalPresence runs on Twenty (open-source CRM) as the system of record — 46 objects, ~1,700 fields, a GraphQL API, with the Legal Studio and the reasoning engine layered on top. A live matter (the vacatur) is in flight. Twenty has produced friction this cycle: a multi-workspace login break, workspace-auth failures, a metadata/enum desync, a 60-row pagination cap, and GraphQL/metadata-API overhead. Much of the stack (ontology mapping, reasoning staging) is already raw Postgres.
Options considered.
| Option | Gains | Costs |
|---|---|---|
| A · Twenty (status quo) | Full CRM UI out of the box; dynamic object/field definition — the data model evolves with no-code and no SQL migrations (well-suited to a still-forming schema); team knows it; the live matter stays put. | Generic sales-CRM UX shoehorned into litigation; metadata layer + its quirks; GraphQL friction. |
| B · Supabase | Unified Postgres (ontology/reasoning stop being side-schemas); standard auth + RLS (per-matter isolation done right); PostgREST/GraphQL auto-API; already self-hosted elsewhere; removes every Twenty-specific trap. | No application UI — must build the front end; a data-access refactor (GraphQL→SQL); migration effort. |
The UX case (the decisive dimension). Twenty's native UX is deals / pipelines / kanban — the wrong vocabulary for counsel, who work in evidence, exhibits, elements, admissions, and filings. A purpose-built litigation UX (matter dashboard, evidence workspace, review queues, motion workspace, testimony digests) is materially better for the actual users — and is the product LegalPresence would need regardless. That UX cannot be Twenty's; it argues for a custom front end on a clean backend (Supabase) if and when LegalPresence becomes a product.
Rationale for staying (PoC). (1) Never re-platform the system of record while a motion is in flight — case-data risk outweighs any cleanliness gain. (2) For a proof-of-concept, Twenty's out-of-box UI is good enough to validate the evidence/reasoning workflow. (3) The migration is mechanical (data is already Postgres) and the refactor simplifies code — so deferring costs little and preserves optionality. (4) The move only pays off once the product question is answered — a post-PoC decision. (5) Twenty's metadata-driven object/field definition is complementary to a PoC whose schema is still evolving — you add and reshape objects and fields with no-code and no migrations, where Supabase would require a SQL migration per schema change.
Consequences. Continue hardening Twenty (fix the metadata desync, keep single-workspace, staging gate live). The ontology + reasoning side-schemas stay Postgres-native — deliberately portable, so a future move is low-friction. No new Supabase dependency now.
Revisit trigger + spike (deferred). When LegalPresence moves from PoC to product (multi-matter, external users, a real counsel UX), run a one-day spike: stand up the core objects (matter, evidenceItem, assertion, contradiction) as clean Supabase tables, copy a data sample, and repoint one Studio screen (the Review Queue) at Supabase — testing "does the code simplify" and "does a purpose-built screen feel right," zero risk to the case. Do the full cutover only after the current matter's filings are complete.
v1.9 — 2026-07-12 — Added the theory layer: theory_engine.py scores affirmative legal theories (contradiction-derived swords and element-anchored grounds) on a four-dimension composite — grounding/stability, element coverage, declared leverage, inverted risk. Every theory, selector, weight and risk penalty is DATA in theories.ttl (no rules hardcoded in the engine); duplicate contradiction pairs are collapsed before counting. Feeds the command-center theory table and the ranked counsel worklist.
v1.8 — 2026-07-09 — Built B-04 reproducibility manifest: model/prompt-hash/extractor-version/source-hash/gen-params on every assertion (PROV-O-style), wired into all extractors (forward-path proven), 913/913 backfilled, projected into RDF.
v1.7 — 2026-07-09 — Added the Audit-readiness gap register (P0/P1/P2, severity·finding·remediation·owner·status), standards mapping (FRE 702/901, RFC 3161, PROV-O, OWASP LLM01, NIST 800-92/53), target-state definitions (assertion states, admissibility tiers, strict motion labels), and the auditor-positioning statement. P0 remediations (ledger hash-chain, tested DB backup, eval-metric baseline) built in core — see status column.
v1.6 — 2026-07-09 — Added "Rules engine — the deterministic spine": the gate/derive/score/conclude roles and the new motion-viability layer (per-motion elements-test AND/OR, contradiction projection + readiness discount, defensive-exposure readout). Built in core: project_contradictions.py, motion_viability.py, rules.py v2026-07-09.4.
v1.5 — 2026-07-09 — Added "What the vault looks like" — three Obsidian sample views (graph-view SVG of the Irreparable-Harm cluster, the rendered element note, the vault tree). Anonymized all worked examples to role labels (Plaintiff / Defendant / Engineer (P.E.) / Architect (AIA) / Witness) — no party names on the page.
v1.4 — 2026-07-09 — Added "The reasoning stack" worked example (document markup → CRM record → SKOS vocabulary → SHACL shape → OTOC-scored reasoning object, with real trimmed snippets) and an Obsidian-value section (graph view over the RDF via export_obsidian_full.py; adds value for metadata/SKOS, not a substitute for SHACL validation or the CRM).
v1.3 — 2026-07-09 — Added ADR-001 (CRM platform — Twenty vs. Supabase): decision to stay on Twenty for the PoC; Supabase deferred to the product transition; noted Twenty's dynamic object/field definition as a complementary strength.
v1.2 — 2026-07-09 — Correction: vault is already IP-allowlisted (deny-all except admin IPs), not publicly routed — hardening item marked done.
v1.1 — 2026-07-09 — Component diagram rendered in Mermaid; public-tier gate documented as by-design (pre-launch Basic auth).
v1.0 — 2026-07-09 — Initial revised three-tier architecture (public / app / infra); staging & promotion boundary for the reasoning engine documented; hardening roadmap (vault-unpublish first).