docs: add architecture visuals and RAMZ tenant isolation decision
Clio/Stripe competitive benchmark, moslem03/04 backend reality check, and the moslem04 sync API spec, plus the resolved schema-per-tenant isolation decision that unblocks Phase 2 of the sync spec.
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
<title>Falah OS — Competitive Benchmark: Clio & Stripe</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f5ef;
|
||||
--bg-panel: #ffffff;
|
||||
--ink: #1b2420;
|
||||
--ink-soft: #4d5951;
|
||||
--line: #dbd9cd;
|
||||
--line-strong: #b9b6a6;
|
||||
--accent: #2f6e52;
|
||||
--accent-soft: #e3ede7;
|
||||
--steal: #2f6e52;
|
||||
--steal-bg: #e3ede7;
|
||||
--build: #a6432e;
|
||||
--build-bg: #f6e6e1;
|
||||
--gap: #9a7327;
|
||||
--gap-bg: #f2e9d4;
|
||||
--mono-tint: #f0efe6;
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #141815;
|
||||
--bg-panel: #1b201c;
|
||||
--ink: #e9e7dd;
|
||||
--ink-soft: #a9b0a7;
|
||||
--line: #333a34;
|
||||
--line-strong: #48524a;
|
||||
--accent: #6bbf94;
|
||||
--accent-soft: #212b24;
|
||||
--steal: #6bbf94;
|
||||
--steal-bg: #1d2b22;
|
||||
--build: #e08268;
|
||||
--build-bg: #2c1e1a;
|
||||
--gap: #e3bd6a;
|
||||
--gap-bg: #2b2416;
|
||||
--mono-tint: #1f2420;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #141815;
|
||||
--bg-panel: #1b201c;
|
||||
--ink: #e9e7dd;
|
||||
--ink-soft: #a9b0a7;
|
||||
--line: #333a34;
|
||||
--line-strong: #48524a;
|
||||
--accent: #6bbf94;
|
||||
--accent-soft: #212b24;
|
||||
--steal: #6bbf94;
|
||||
--steal-bg: #1d2b22;
|
||||
--build: #e08268;
|
||||
--build-bg: #2c1e1a;
|
||||
--gap: #e3bd6a;
|
||||
--gap-bg: #2b2416;
|
||||
--mono-tint: #1f2420;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Source Serif Pro", serif;
|
||||
}
|
||||
body {
|
||||
padding: clamp(1.5rem, 4vw, 4rem);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 920px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: clamp(2.5rem, 5vw, 3.5rem);
|
||||
}
|
||||
|
||||
.masthead {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
padding-bottom: 1.75rem;
|
||||
border-bottom: 2px solid var(--ink);
|
||||
}
|
||||
.eyebrow {
|
||||
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.9rem, 4vw, 2.6rem);
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
.dek {
|
||||
margin: 0;
|
||||
max-width: 62ch;
|
||||
font-size: 1.02rem;
|
||||
line-height: 1.55;
|
||||
color: var(--ink-soft);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
section.bench {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.1rem;
|
||||
}
|
||||
.bench-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bench-title {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.bench-title h2 {
|
||||
margin: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bench-title .vs {
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 0.85rem;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.bench-note {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 0.82rem;
|
||||
color: var(--ink-soft);
|
||||
max-width: 40ch;
|
||||
text-align: right;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 3px;
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
min-width: 640px;
|
||||
border-collapse: collapse;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
thead th {
|
||||
text-align: left;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-soft);
|
||||
padding: 0.7rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line-strong);
|
||||
background: var(--mono-tint);
|
||||
}
|
||||
tbody td {
|
||||
padding: 0.75rem 0.9rem;
|
||||
border-bottom: 1px solid var(--line);
|
||||
vertical-align: top;
|
||||
line-height: 1.45;
|
||||
}
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
|
||||
td.area {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: var(--ink);
|
||||
}
|
||||
td.detail { color: var(--ink-soft); }
|
||||
td.verdict { width: 15%; }
|
||||
|
||||
.chip {
|
||||
display: inline-block;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
padding: 0.22rem 0.55rem;
|
||||
border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chip.steal { color: var(--steal); background: var(--steal-bg); }
|
||||
.chip.build { color: var(--build); background: var(--build-bg); }
|
||||
.chip.gap { color: var(--gap); background: var(--gap-bg); }
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 1.4rem;
|
||||
flex-wrap: wrap;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 0.78rem;
|
||||
color: var(--ink-soft);
|
||||
align-items: center;
|
||||
}
|
||||
.legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
|
||||
|
||||
.synth {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 3px;
|
||||
background: var(--bg-panel);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.synth { grid-template-columns: 1fr; }
|
||||
}
|
||||
.synth-col h3 {
|
||||
margin: 0 0 0.85rem 0;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.synth-col.steal h3 { color: var(--steal); }
|
||||
.synth-col.build h3 { color: var(--build); }
|
||||
.synth ol {
|
||||
margin: 0;
|
||||
padding: 0 0 0 1.15rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.55;
|
||||
color: var(--ink);
|
||||
}
|
||||
.synth ol li::marker {
|
||||
font-family: ui-monospace, monospace;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
.synth li + li { margin-top: 0.4rem; }
|
||||
|
||||
.divider {
|
||||
grid-column: 1 / -1;
|
||||
height: 1px;
|
||||
background: var(--line);
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.divider { display: block; }
|
||||
}
|
||||
|
||||
footer {
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--line);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 0.78rem;
|
||||
color: var(--ink-soft);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
footer strong { color: var(--ink); }
|
||||
</style>
|
||||
|
||||
<main>
|
||||
<div class="masthead">
|
||||
<span class="eyebrow">Falah OS — Nur Falah Admin Console</span>
|
||||
<h1>Competitive benchmark: Clio & Stripe Dashboard</h1>
|
||||
<p class="dek">Two reference products mapped against EstateOS and the cross-tenant super-admin surface. Every row ends in a call: <strong style="color:var(--steal)">steal</strong> the pattern, <strong style="color:var(--build)">build</strong> it ourselves, or note it as a <strong style="color:var(--gap)">structural gap</strong> the reference product simply doesn't resolve.</p>
|
||||
</div>
|
||||
|
||||
<section class="bench">
|
||||
<div class="bench-head">
|
||||
<div class="bench-title">
|
||||
<h2>Clio</h2>
|
||||
<span class="vs">vs. EstateOS</span>
|
||||
</div>
|
||||
<p class="bench-note">Legal practice management — case files, billing, client portal, co-counsel collaboration.</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Feature area</th><th>What Clio does</th><th>Verdict for EstateOS</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="area">Intake → billing</td>
|
||||
<td class="detail">Clio Grow captures leads via custom forms, hands off to Clio Manage automatically once a matter opens.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Document automation</td>
|
||||
<td class="detail">Template library with variable-filled document generation, version control.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Billing & invoicing</td>
|
||||
<td class="detail">Timers, hourly/contingency billing, AI-drafted invoices with error flagging.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Trust accounting</td>
|
||||
<td class="detail">Client trust ledgers, deposit/disbursement tracking, compliance reporting.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Client portal</td>
|
||||
<td class="detail">Encrypted messaging, document sharing, payments — but single-brand, one firm per instance.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal UX</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Faraid / Syariah workflows</td>
|
||||
<td class="detail">No inheritance-share calculation, no Syariah Court filing templates — generic common-law matter types only.</td>
|
||||
<td class="verdict"><span class="chip build">Build</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Panel-lawyer vetting</td>
|
||||
<td class="detail">"Clio for Co-Counsel" assumes counsel is already chosen — it's a collaboration portal, not a credentialing tool.</td>
|
||||
<td class="verdict"><span class="chip build">Build</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Per-tenant branding</td>
|
||||
<td class="detail">Client portal is single-brand per firm — no white-label capability for a platform serving multiple firms.</td>
|
||||
<td class="verdict"><span class="chip build">Build</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Multi-tenant backend</td>
|
||||
<td class="detail">Clio is one firm, one instance — no shared-backend, multi-firm architecture exists to compare against.</td>
|
||||
<td class="verdict"><span class="chip gap">Structural gap</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">RAMZ row-level access</td>
|
||||
<td class="detail">Firm-scoped permissions throughout — no concept of row-level, cross-tenant gating.</td>
|
||||
<td class="verdict"><span class="chip gap">Structural gap</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="bench">
|
||||
<div class="bench-head">
|
||||
<div class="bench-title">
|
||||
<h2>Stripe Dashboard</h2>
|
||||
<span class="vs">vs. Falah OS super-admin</span>
|
||||
</div>
|
||||
<p class="bench-note">Connect platform admin — connected-account management, roles, audit, API access.</p>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Feature area</th><th>What Stripe does</th><th>Verdict for Falah OS</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="area">Admin role tiers</td>
|
||||
<td class="detail">Admin vs. Super Administrator — Super Admin gates who can grant/revoke roles and manage environments.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Audit logging</td>
|
||||
<td class="detail">Activity Logs API — exportable, SOC 2/PCI-ready record of key changes, invitations, role changes.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">API key scoping</td>
|
||||
<td class="detail">Restricted keys limited to specific permission sets per integration.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Tenant status view</td>
|
||||
<td class="detail">Connected-account list with onboarding and risk status at a glance.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal UX</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Onboarding flow</td>
|
||||
<td class="detail">Hosted or embedded onboarding, incremental — collects only what's currently required.</td>
|
||||
<td class="verdict"><span class="chip steal">Steal</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Cross-regime compliance routing</td>
|
||||
<td class="detail">No concept of routing tenants through different regulatory rule sets (Syariah Court vs. JKPTG vs. govt case routing).</td>
|
||||
<td class="verdict"><span class="chip build">Build</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Usage / billing-trigger events</td>
|
||||
<td class="detail">Activity Logs are security-focused, not usage-metering. Stripe's actual metering product wasn't covered — separate follow-up needed.</td>
|
||||
<td class="verdict"><span class="chip build">Build / unclear</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">Tenant = product line</td>
|
||||
<td class="detail">Every connected account is a roughly uniform merchant entity — no support for managing distinct SaaS products per tenant.</td>
|
||||
<td class="verdict"><span class="chip gap">Structural gap</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="area">RAMZ row-level access</td>
|
||||
<td class="detail">Isolation happens at the account level (own object graph per tenant) — closer to separate-DB than row-level gating.</td>
|
||||
<td class="verdict"><span class="chip gap">Structural gap</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="legend">
|
||||
<span><span class="chip steal">Steal</span> pattern transfers directly</span>
|
||||
<span><span class="chip build">Build</span> reference has nothing comparable</span>
|
||||
<span><span class="chip gap">Structural gap</span> reference's whole model doesn't apply</span>
|
||||
</div>
|
||||
|
||||
<section class="synth">
|
||||
<div class="synth-col steal">
|
||||
<h3>Combined — steal list</h3>
|
||||
<ol>
|
||||
<li>Intake → billing handoff (Clio Grow → Manage)</li>
|
||||
<li>Document automation + template library</li>
|
||||
<li>Trust ledger compliance reporting</li>
|
||||
<li>Two-tier Admin / Super Admin roles</li>
|
||||
<li>Exportable audit log, scoped API keys</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="synth-col build">
|
||||
<h3>Combined — build list</h3>
|
||||
<ol>
|
||||
<li>Faraid inheritance calculation engine</li>
|
||||
<li>Panel-lawyer vetting / credentialing</li>
|
||||
<li>Per-tenant white-labeled portal branding</li>
|
||||
<li>RAMZ row-level, shared-schema isolation</li>
|
||||
<li>Cross-regime compliance routing (Syariah / JKPTG / govt)</li>
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<span>Sources: clio.com, docs.stripe.com, support.stripe.com — full citations in the prior research turns.</span>
|
||||
<span><strong>Open:</strong> Core Ledger billing-event readiness still unconfirmed against Stripe's metering product.</span>
|
||||
</footer>
|
||||
</main>
|
||||
@@ -0,0 +1,259 @@
|
||||
<title>Falah OS — Backend Integration Reality Check</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f5ef;
|
||||
--bg-panel: #ffffff;
|
||||
--ink: #1b2420;
|
||||
--ink-soft: #4d5951;
|
||||
--line: #dbd9cd;
|
||||
--line-strong: #b9b6a6;
|
||||
--accent: #2f6e52;
|
||||
--accent-soft: #e3ede7;
|
||||
--warn: #a6432e;
|
||||
--warn-bg: #f6e6e1;
|
||||
--future: #9a7327;
|
||||
--future-bg: #f2e9d4;
|
||||
--node-bg: #ffffff;
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #141815;
|
||||
--bg-panel: #1b201c;
|
||||
--ink: #e9e7dd;
|
||||
--ink-soft: #a9b0a7;
|
||||
--line: #333a34;
|
||||
--line-strong: #48524a;
|
||||
--accent: #6bbf94;
|
||||
--accent-soft: #212b24;
|
||||
--warn: #e08268;
|
||||
--warn-bg: #2c1e1a;
|
||||
--future: #e3bd6a;
|
||||
--future-bg: #2b2416;
|
||||
--node-bg: #1f2420;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #141815;
|
||||
--bg-panel: #1b201c;
|
||||
--ink: #e9e7dd;
|
||||
--ink-soft: #a9b0a7;
|
||||
--line: #333a34;
|
||||
--line-strong: #48524a;
|
||||
--accent: #6bbf94;
|
||||
--accent-soft: #212b24;
|
||||
--warn: #e08268;
|
||||
--warn-bg: #2c1e1a;
|
||||
--future: #e3bd6a;
|
||||
--future-bg: #2b2416;
|
||||
--node-bg: #1f2420;
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; background: var(--bg); color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
|
||||
body { padding: clamp(1.25rem, 4vw, 3rem); display: flex; justify-content: center; }
|
||||
main { width: 100%; max-width: 900px; display: flex; flex-direction: column; gap: 2.5rem; }
|
||||
|
||||
.masthead { display: flex; flex-direction: column; gap: 0.5rem; padding-bottom: 1.25rem; border-bottom: 2px solid var(--ink); }
|
||||
.eyebrow { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); }
|
||||
h1 { margin: 0; font-size: clamp(1.5rem, 3.4vw, 2rem); font-weight: 700; letter-spacing: -0.01em; text-wrap: balance; }
|
||||
.dek { margin: 0; max-width: 60ch; font-size: 0.98rem; line-height: 1.5; color: var(--ink-soft); }
|
||||
|
||||
h2.section-title {
|
||||
display: flex; align-items: center; gap: 0.6rem;
|
||||
font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
|
||||
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
color: var(--ink-soft); margin: 0 0 1.1rem 0;
|
||||
}
|
||||
h2.section-title .n { color: var(--ink); font-weight: 700; }
|
||||
|
||||
/* ---- Diagram 1: today ---- */
|
||||
.diagram {
|
||||
border: 1px solid var(--line-strong);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-panel);
|
||||
padding: clamp(1.25rem, 3vw, 2rem);
|
||||
overflow-x: auto;
|
||||
}
|
||||
.row { display: flex; align-items: center; gap: 0; min-width: 640px; }
|
||||
.node {
|
||||
flex: 1;
|
||||
border: 1.5px solid var(--line-strong);
|
||||
border-radius: 6px;
|
||||
background: var(--node-bg);
|
||||
padding: 0.9rem 1rem;
|
||||
text-align: center;
|
||||
display: flex; flex-direction: column; gap: 0.25rem;
|
||||
}
|
||||
.node .label { font-weight: 700; font-size: 0.92rem; }
|
||||
.node .sub { font-size: 0.74rem; color: var(--ink-soft); }
|
||||
.arrow {
|
||||
flex: 0 0 60px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: ui-monospace, monospace;
|
||||
color: var(--ink-soft);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.node.app { border-color: var(--accent); background: var(--accent-soft); }
|
||||
.node.api { border-style: dashed; }
|
||||
.node.void { border-color: var(--warn); background: var(--warn-bg); border-style: dashed; color: var(--warn); }
|
||||
.node.void .label { color: var(--warn); }
|
||||
|
||||
.three-up { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.9rem; margin-top: 1rem; min-width: 640px; }
|
||||
.three-up .node .sub { line-height: 1.35; }
|
||||
|
||||
.badge-row { display: flex; gap: 0.6rem; margin-top: 1.1rem; flex-wrap: wrap; }
|
||||
.badge {
|
||||
font-family: ui-monospace, monospace; font-size: 0.68rem; font-weight: 700;
|
||||
letter-spacing: 0.04em; text-transform: uppercase;
|
||||
padding: 0.25rem 0.6rem; border-radius: 3px;
|
||||
}
|
||||
.badge.no { color: var(--warn); background: var(--warn-bg); }
|
||||
|
||||
/* ---- Diagram 2: what needs building ---- */
|
||||
.stack { display: flex; flex-direction: column; gap: 0; min-width: 560px; }
|
||||
.layer {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 1fr;
|
||||
border: 1.5px solid var(--line-strong);
|
||||
border-top: none;
|
||||
background: var(--node-bg);
|
||||
}
|
||||
.layer:first-child { border-top: 1.5px solid var(--line-strong); border-radius: 6px 6px 0 0; overflow: hidden; }
|
||||
.layer:last-child { border-radius: 0 0 6px 6px; overflow: hidden; }
|
||||
.layer-num {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-family: ui-monospace, monospace; font-size: 0.78rem; font-weight: 700;
|
||||
color: var(--ink-soft); background: color-mix(in srgb, var(--ink) 4%, transparent);
|
||||
border-right: 1px solid var(--line);
|
||||
}
|
||||
.layer-body { padding: 0.85rem 1.1rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
||||
.layer-body .title { font-weight: 700; font-size: 0.92rem; }
|
||||
.layer-body .desc { font-size: 0.8rem; color: var(--ink-soft); }
|
||||
.layer.exists { }
|
||||
.layer.exists .layer-num { color: var(--accent); }
|
||||
.layer.missing .layer-num { color: var(--future); }
|
||||
.status {
|
||||
font-family: ui-monospace, monospace; font-size: 0.66rem; font-weight: 700;
|
||||
letter-spacing: 0.05em; text-transform: uppercase; padding: 0.18rem 0.5rem; border-radius: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.status.exists { color: var(--accent); background: var(--accent-soft); }
|
||||
.status.missing { color: var(--future); background: var(--future-bg); }
|
||||
|
||||
.legend { display: flex; gap: 1.3rem; flex-wrap: wrap; font-size: 0.78rem; color: var(--ink-soft); margin-top: 1rem; }
|
||||
.legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
|
||||
.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
|
||||
.dot.exists { background: var(--accent); }
|
||||
.dot.missing { background: var(--future); }
|
||||
|
||||
footer { padding-top: 1.25rem; border-top: 1px solid var(--line); font-size: 0.78rem; color: var(--ink-soft); }
|
||||
footer strong { color: var(--ink); }
|
||||
</style>
|
||||
|
||||
<main>
|
||||
<div class="masthead">
|
||||
<span class="eyebrow">Falah OS — checked against the actual repos</span>
|
||||
<h1>moslem03 has no backend either — same gap as moslem04</h1>
|
||||
<p class="dek">Verified against <code>nur-muslim-companion-v2</code> and <code>nur-falah-prevention</code> source. Neither app talks to a Falah OS server. There is no existing pattern to copy — the admin console link has to be built from zero.</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title"><span class="n">01</span> · What moslem03 actually does today</h2>
|
||||
<div class="diagram">
|
||||
<div class="row">
|
||||
<div class="node app">
|
||||
<span class="label">moslem03.falahos.my</span>
|
||||
<span class="sub">Svelte 5 PWA · static build</span>
|
||||
</div>
|
||||
<div class="arrow">→</div>
|
||||
<div class="node api">
|
||||
<span class="label">Public third-party APIs</span>
|
||||
<span class="sub">Al Quran Cloud · Aladhan · Overpass</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="three-up">
|
||||
<div class="node void">
|
||||
<span class="label">✕ No auth</span>
|
||||
<span class="sub">Anyone opens the app, no login</span>
|
||||
</div>
|
||||
<div class="node void">
|
||||
<span class="label">✕ No Falah OS server</span>
|
||||
<span class="sub">Never calls anything on the Contabo VPS</span>
|
||||
</div>
|
||||
<div class="node void">
|
||||
<span class="label">✕ No admin console link</span>
|
||||
<span class="sub">Nothing to sync — data never leaves the browser</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="badge-row">
|
||||
<span class="badge no">Same for moslem04</span>
|
||||
<span class="badge no">Same localStorage-only pattern</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title"><span class="n">02</span> · What has to be built — bottom up</h2>
|
||||
<div class="diagram">
|
||||
<div class="stack">
|
||||
<div class="layer exists">
|
||||
<div class="layer-num">✓</div>
|
||||
<div class="layer-body">
|
||||
<div>
|
||||
<div class="title">casdoor (ummahid) identity service</div>
|
||||
<div class="desc">Already running on the Contabo VPS :8000 — reuse it, don't rebuild login</div>
|
||||
</div>
|
||||
<span class="status exists">Already exists</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer missing">
|
||||
<div class="layer-num">1</div>
|
||||
<div class="layer-body">
|
||||
<div>
|
||||
<div class="title">Login screen in moslem04</div>
|
||||
<div class="desc">App currently has zero auth UI — needs a casdoor sign-in flow bolted on</div>
|
||||
</div>
|
||||
<span class="status missing">To build</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer missing">
|
||||
<div class="layer-num">2</div>
|
||||
<div class="layer-body">
|
||||
<div>
|
||||
<div class="title">Sync API (new)</div>
|
||||
<div class="desc">Push Faraid / Assets / Wassiyah / Hibah / Waqf drafts from localStorage to a server, encrypted</div>
|
||||
</div>
|
||||
<span class="status missing">To build</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer missing">
|
||||
<div class="layer-num">3</div>
|
||||
<div class="layer-body">
|
||||
<div>
|
||||
<div class="title">RAMZ tenant tagging</div>
|
||||
<div class="desc">Every synced record gets stamped with which tenant/firm it belongs to</div>
|
||||
</div>
|
||||
<span class="status missing">To build</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layer missing">
|
||||
<div class="layer-num">4</div>
|
||||
<div class="layer-body">
|
||||
<div>
|
||||
<div class="title">Admin console read view</div>
|
||||
<div class="desc">Nur Falah Admin Console reads synced cases — view only, not edit</div>
|
||||
</div>
|
||||
<span class="status missing">To build</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span><span class="dot exists"></span> exists on the VPS today</span>
|
||||
<span><span class="dot missing"></span> needs to be built — nothing to copy from moslem03</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<strong>Bottom line:</strong> moslem03 gave us one reusable piece — casdoor login. Everything else (sync, tenant tagging, admin visibility) starts from scratch.
|
||||
</footer>
|
||||
</main>
|
||||
@@ -0,0 +1,58 @@
|
||||
# RAMZ Tenant Isolation — Decision
|
||||
|
||||
**Status:** Resolved
|
||||
**Blocks:** Phase 2 of the [moslem04 Sync API spec](./sync-api-spec.html) (admin console read view)
|
||||
**Originally flagged in:** Nur Falah Admin Console PRD review
|
||||
|
||||
## The question
|
||||
|
||||
Falah OS runs one shared backend serving three tenants — EstateOS (law firm estate/probate,
|
||||
Syariah Court), Tanah Hidup (waqf land lease, JKPTG), and Cair (govt case routing). How is
|
||||
each tenant's data isolated from the others?
|
||||
|
||||
Two options were on the table:
|
||||
|
||||
1. **Separate database per tenant** — strongest isolation, highest ops overhead.
|
||||
2. **Row-level RAMZ-gated access in a shared schema** — cheapest, weakest isolation story.
|
||||
|
||||
## Decision
|
||||
|
||||
**Schema-per-tenant on a single shared Postgres cluster.**
|
||||
|
||||
Each tenant (EstateOS, Tanah Hidup, Cair) gets its own Postgres schema. Not a separate
|
||||
server, not a shared table filtered by a `tenant_id` column.
|
||||
|
||||
## Why
|
||||
|
||||
- The three tenants aren't multiple firms sharing one product (the Clio model) — they're
|
||||
three structurally different products, each answering to a different regulator
|
||||
(Syariah Court, JKPTG, government agencies). A `WHERE tenant_id = ?` filter on a shared
|
||||
table is not an answer a regulator's auditor will accept at face value; a schema
|
||||
boundary is something that can actually be demonstrated.
|
||||
- This was flagged as a trust/compliance blocker for the UK Innovator Visa pitch — the
|
||||
isolation model isn't just an engineering choice, it's part of the pitch.
|
||||
- Fully separate databases (or separate servers) solve the same trust problem but multiply
|
||||
ops burden — separate backup policies, connection pools, migrations to run three times
|
||||
instead of once. Not justified at current scale.
|
||||
- Schema-per-tenant keeps one cluster, one backup policy, one connection pool — while still
|
||||
giving each tenant a real, auditable namespace boundary.
|
||||
|
||||
## What RAMZ still does
|
||||
|
||||
RAMZ doesn't disappear — it moves down a level. Within each tenant's schema, RAMZ handles
|
||||
the finer-grained access control that a schema boundary can't: which user or law firm
|
||||
within EstateOS can see which case, which land officer within Tanah Hidup can see which
|
||||
lease. RAMZ was never going to be strong enough to be the *only* thing standing between
|
||||
tenants — now it doesn't have to be.
|
||||
|
||||
## Consequence for the sync API
|
||||
|
||||
The `tenant_id` field in the [sync spec](./sync-api-spec.html) becomes a schema selector,
|
||||
not a row filter. The admin console's `/v1/admin/metadata` endpoint queries across schemas
|
||||
explicitly, per-tenant — there is no accidental cross-tenant query path.
|
||||
|
||||
## Still open
|
||||
|
||||
This resolves the isolation *model*. It does not resolve whether Core Ledger already emits
|
||||
the billing-trigger events Phase 2 needs, or who owns panel-lawyer vetting — both remain
|
||||
open from the earlier PRD review.
|
||||
@@ -0,0 +1,224 @@
|
||||
<title>moslem04 Sync API — Architecture Spec</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f5ef;
|
||||
--bg-panel: #ffffff;
|
||||
--ink: #1b2420;
|
||||
--ink-soft: #4d5951;
|
||||
--line: #dbd9cd;
|
||||
--line-strong: #b9b6a6;
|
||||
--accent: #2f6e52;
|
||||
--accent-soft: #e3ede7;
|
||||
--warn: #a6432e;
|
||||
--warn-bg: #f6e6e1;
|
||||
--future: #9a7327;
|
||||
--future-bg: #f2e9d4;
|
||||
--mono-tint: #f0efe6;
|
||||
--node-bg: #ffffff;
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--bg: #141815; --bg-panel: #1b201c; --ink: #e9e7dd; --ink-soft: #a9b0a7;
|
||||
--line: #333a34; --line-strong: #48524a; --accent: #6bbf94; --accent-soft: #212b24;
|
||||
--warn: #e08268; --warn-bg: #2c1e1a; --future: #e3bd6a; --future-bg: #2b2416;
|
||||
--mono-tint: #1f2420; --node-bg: #1f2420;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--bg: #141815; --bg-panel: #1b201c; --ink: #e9e7dd; --ink-soft: #a9b0a7;
|
||||
--line: #333a34; --line-strong: #48524a; --accent: #6bbf94; --accent-soft: #212b24;
|
||||
--warn: #e08268; --warn-bg: #2c1e1a; --future: #e3bd6a; --future-bg: #2b2416;
|
||||
--mono-tint: #1f2420; --node-bg: #1f2420;
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; background: var(--bg); color: var(--ink); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif; }
|
||||
body { padding: clamp(1.25rem, 4vw, 3rem); display: flex; justify-content: center; }
|
||||
main { width: 100%; max-width: 940px; display: flex; flex-direction: column; gap: 2.75rem; }
|
||||
code, .mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; }
|
||||
|
||||
/* masthead */
|
||||
.masthead { display: flex; flex-direction: column; gap: 0.6rem; padding-bottom: 1.4rem; border-bottom: 2px solid var(--ink); }
|
||||
.eyebrow { font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--accent); font-family: ui-monospace, monospace; }
|
||||
h1 { margin: 0; font-size: clamp(1.6rem, 3.6vw, 2.2rem); font-weight: 700; letter-spacing: -0.01em; text-wrap: balance; }
|
||||
.dek { margin: 0; max-width: 68ch; font-size: 0.98rem; line-height: 1.55; color: var(--ink-soft); }
|
||||
.scope-note {
|
||||
margin-top: 0.4rem; display: flex; gap: 0.6rem; align-items: flex-start;
|
||||
font-size: 0.85rem; padding: 0.7rem 0.9rem; background: var(--future-bg); border-radius: 4px;
|
||||
border-left: 3px solid var(--future); color: var(--ink);
|
||||
}
|
||||
.scope-note strong { color: var(--future); }
|
||||
|
||||
h2.section-title {
|
||||
display: flex; align-items: baseline; gap: 0.6rem;
|
||||
font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
|
||||
font-family: ui-monospace, monospace; color: var(--ink-soft); margin: 0 0 1.1rem 0;
|
||||
}
|
||||
h2.section-title .n { color: var(--accent); font-weight: 700; }
|
||||
h2.section-title .t { color: var(--ink); font-size: 0.78rem; letter-spacing: 0.08em; }
|
||||
|
||||
p.body-text { font-size: 0.92rem; line-height: 1.6; color: var(--ink); max-width: 68ch; margin: 0 0 0.9rem 0; }
|
||||
p.body-text:last-child { margin-bottom: 0; }
|
||||
|
||||
/* flow diagram */
|
||||
.diagram { border: 1px solid var(--line-strong); border-radius: 6px; background: var(--bg-panel); padding: clamp(1.1rem, 3vw, 1.6rem); overflow-x: auto; }
|
||||
.flow { display: flex; align-items: stretch; gap: 0; min-width: 760px; }
|
||||
.fnode {
|
||||
flex: 1; border: 1.5px solid var(--line-strong); border-radius: 6px; background: var(--node-bg);
|
||||
padding: 0.85rem 0.9rem; display: flex; flex-direction: column; gap: 0.3rem; justify-content: center;
|
||||
}
|
||||
.fnode .label { font-weight: 700; font-size: 0.85rem; }
|
||||
.fnode .sub { font-size: 0.72rem; color: var(--ink-soft); line-height: 1.35; }
|
||||
.fnode.client { border-color: var(--accent); background: var(--accent-soft); }
|
||||
.fnode.new { border-style: dashed; border-color: var(--future); }
|
||||
.fnode.new .label { color: var(--future); }
|
||||
.fnode.gated { border-color: var(--warn); background: var(--warn-bg); }
|
||||
.fnode.gated .label { color: var(--warn); }
|
||||
.farrow { flex: 0 0 34px; display: flex; align-items: center; justify-content: center; color: var(--ink-soft); font-size: 1.1rem; }
|
||||
|
||||
/* tables */
|
||||
.table-wrap { overflow-x: auto; border: 1px solid var(--line-strong); border-radius: 6px; background: var(--bg-panel); }
|
||||
table { width: 100%; min-width: 600px; border-collapse: collapse; font-size: 0.85rem; }
|
||||
thead th {
|
||||
text-align: left; font-family: ui-monospace, monospace; font-size: 0.66rem; letter-spacing: 0.08em;
|
||||
text-transform: uppercase; color: var(--ink-soft); padding: 0.65rem 0.85rem; border-bottom: 1px solid var(--line-strong);
|
||||
background: var(--mono-tint);
|
||||
}
|
||||
tbody td { padding: 0.7rem 0.85rem; border-bottom: 1px solid var(--line); vertical-align: top; line-height: 1.45; }
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
td.method { font-family: ui-monospace, monospace; font-weight: 700; font-size: 0.76rem; white-space: nowrap; }
|
||||
td.path { font-family: ui-monospace, monospace; font-size: 0.8rem; white-space: nowrap; color: var(--ink); }
|
||||
td.detail { color: var(--ink-soft); }
|
||||
.m-get { color: var(--accent); } .m-post { color: #4d6fb3; } .m-delete { color: var(--warn); }
|
||||
:root[data-theme="dark"] .m-post, .m-post { color: #7fa2e6; }
|
||||
|
||||
.chip { display: inline-block; font-family: ui-monospace, monospace; font-size: 0.64rem; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; padding: 0.18rem 0.5rem; border-radius: 3px; white-space: nowrap; }
|
||||
.chip.locked { color: var(--warn); background: var(--warn-bg); }
|
||||
.chip.opt { color: var(--future); background: var(--future-bg); }
|
||||
.chip.never { color: var(--ink-soft); background: var(--mono-tint); }
|
||||
|
||||
/* rollout */
|
||||
.phases { display: flex; gap: 0; min-width: 700px; }
|
||||
.phase {
|
||||
flex: 1; border: 1.5px solid var(--line-strong); background: var(--node-bg); padding: 1rem;
|
||||
display: flex; flex-direction: column; gap: 0.4rem; position: relative;
|
||||
}
|
||||
.phase:not(:first-child) { border-left: none; }
|
||||
.phase:first-child { border-radius: 6px 0 0 6px; }
|
||||
.phase:last-child { border-radius: 0 6px 6px 0; }
|
||||
.phase .ph-label { font-family: ui-monospace, monospace; font-size: 0.66rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--accent); }
|
||||
.phase .ph-title { font-weight: 700; font-size: 0.88rem; }
|
||||
.phase .ph-desc { font-size: 0.78rem; color: var(--ink-soft); line-height: 1.4; }
|
||||
.phase.blocked { background: var(--warn-bg); }
|
||||
.phase.blocked .ph-label { color: var(--warn); }
|
||||
|
||||
footer { padding-top: 1.4rem; border-top: 1px solid var(--line); font-size: 0.8rem; color: var(--ink-soft); line-height: 1.6; }
|
||||
footer strong { color: var(--ink); }
|
||||
</style>
|
||||
|
||||
<main>
|
||||
<div class="masthead">
|
||||
<span class="eyebrow">Falah OS — Nur Falah Estate & Waqf Suite</span>
|
||||
<h1>Sync API architecture — moslem04.falahos.my</h1>
|
||||
<p class="dek">A server-side sync layer for moslem04's six local-first modules, gated by casdoor login, encrypted client-side before it ever leaves the device, and surfacing only opt-in metadata to the admin console — never raw estate documents.</p>
|
||||
<div class="scope-note"><strong>Out of scope:</strong> moslem03 (Muslim Companion) stays exactly as-is — free, zero signup, no account, no sync, forever. This spec touches nothing there.</div>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title"><span class="n">01</span><span class="t">Data flow, end to end</span></h2>
|
||||
<div class="diagram">
|
||||
<div class="flow">
|
||||
<div class="fnode client">
|
||||
<span class="label">moslem04 (browser)</span>
|
||||
<span class="sub">localStorage stays the source of truth offline; sync is additive, not a replacement</span>
|
||||
</div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fnode">
|
||||
<span class="label">casdoor (existing)</span>
|
||||
<span class="sub">OIDC login, issues JWT — nothing new to build here</span>
|
||||
</div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fnode new">
|
||||
<span class="label">Sync API (new)</span>
|
||||
<span class="sub">Accepts encrypted blobs, tags with tenant + user, never decrypts</span>
|
||||
</div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fnode new">
|
||||
<span class="label">Postgres (new)</span>
|
||||
<span class="sub">Ciphertext + RAMZ tenant column + a small plaintext metadata row</span>
|
||||
</div>
|
||||
<div class="farrow">→</div>
|
||||
<div class="fnode gated">
|
||||
<span class="label">Admin console</span>
|
||||
<span class="sub">Reads metadata only — opt-in, read-only, per Phase 2 below</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title"><span class="n">02</span><span class="t">What gets encrypted vs. what admin can see</span></h2>
|
||||
<p class="body-text">Estate plans are the most sensitive data a person will put into this app. The sync API is built so the server — and by extension the admin console — physically cannot read the contents of anyone's Faraid, Wassiyah, Hibah, or Waqf drafts. Encryption happens on-device before upload.</p>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Field</th><th>Who can read it</th><th>Notes</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="mono">payload_ciphertext</td><td><span class="chip locked">User only</span></td><td class="detail">AES-GCM, key derived from casdoor session + on-device passphrase. Server stores bytes it cannot open.</td></tr>
|
||||
<tr><td class="mono">module</td><td><span class="chip opt">User + admin</span></td><td class="detail">Which of the 6 modules the record belongs to — plaintext, needed for routing and counts only.</td></tr>
|
||||
<tr><td class="mono">plan_completed</td><td><span class="chip opt">User + admin</span></td><td class="detail">Boolean per module. Lets the console show "how many people finished their Wassiyah" without seeing a single will.</td></tr>
|
||||
<tr><td class="mono">panel_lawyer_requested</td><td><span class="chip opt">User + admin</span></td><td class="detail">Explicit opt-in flag the user sets — the only trigger that surfaces a person to a panel lawyer at all.</td></tr>
|
||||
<tr><td class="mono">tenant_id (RAMZ)</td><td><span class="chip opt">Admin only</span></td><td class="detail">Which tenant/firm this record is scoped to — still pending the row-level isolation decision from the PRD review.</td></tr>
|
||||
<tr><td class="mono">claims_payload (H2)</td><td><span class="chip never">Nobody, not synced</span></td><td class="detail">Horizon 2 Claims module is excluded from sync entirely until the Phase 0 legal gate in DEPLOY.md clears.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title"><span class="n">03</span><span class="t">Endpoints</span></h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>Method</th><th>Path</th><th>Purpose</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td class="method m-post">POST</td><td class="path">/v1/auth/exchange</td><td class="detail">Trade a casdoor OIDC token for a moslem04 session token</td></tr>
|
||||
<tr><td class="method m-get">GET</td><td class="path">/v1/sync/pull?since=<ts></td><td class="detail">Fetch records changed since last sync — for restoring on a new device</td></tr>
|
||||
<tr><td class="method m-post">POST</td><td class="path">/v1/sync/push</td><td class="detail">Batch upsert of encrypted blobs from the local device queue</td></tr>
|
||||
<tr><td class="method m-delete">DELETE</td><td class="path">/v1/sync/:record_id</td><td class="detail">Tombstone a record — deletes propagate across the user's own devices</td></tr>
|
||||
<tr><td class="method m-post">POST</td><td class="path">/v1/consent/panel-lawyer</td><td class="detail">The one explicit opt-in action that makes a user visible to EstateOS admin at all</td></tr>
|
||||
<tr><td class="method m-get">GET</td><td class="path">/v1/admin/metadata</td><td class="detail">Admin console read — returns only the plaintext metadata fields from §02, RAMZ-scoped</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title"><span class="n">04</span><span class="t">Conflict handling</span></h2>
|
||||
<p class="body-text">These are single-owner personal drafts, not shared documents — so last-write-wins by <code>updated_at</code> is sufficient for five of the six modules. Two exceptions: <strong>Family Waqf Designator</strong> can involve co-designators, so pushes there append to a change log instead of overwriting; <strong>Digital Claims (H2)</strong> doesn't sync at all, see §02.</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 class="section-title"><span class="n">05</span><span class="t">Rollout phases</span></h2>
|
||||
<div class="diagram">
|
||||
<div class="phases">
|
||||
<div class="phase">
|
||||
<span class="ph-label">Phase 0</span>
|
||||
<span class="ph-title">Auth + sync plumbing</span>
|
||||
<span class="ph-desc">Login, encrypted push/pull, cross-device restore. No admin visibility of any kind.</span>
|
||||
</div>
|
||||
<div class="phase">
|
||||
<span class="ph-label">Phase 1</span>
|
||||
<span class="ph-title">Opt-in metadata surface</span>
|
||||
<span class="ph-desc">Panel-lawyer request flag goes live. Admin sees only what §02 allows.</span>
|
||||
</div>
|
||||
<div class="phase blocked">
|
||||
<span class="ph-label">Phase 2 · blocked</span>
|
||||
<span class="ph-title">Full admin console read view</span>
|
||||
<span class="ph-desc">Waits on the RAMZ isolation decision and Core Ledger billing-event readiness — both still open from the earlier PRD review.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<strong>Carried-over open questions this spec does not resolve:</strong> row-level (RAMZ) vs. separate-DB tenant isolation, whether Core Ledger already emits the billing-trigger events Phase 2 would need, and who owns panel-lawyer vetting once requests start arriving. All three block Phase 2, not Phase 0–1.
|
||||
</footer>
|
||||
</main>
|
||||
Reference in New Issue
Block a user