diff --git a/docs/architecture/benchmark-clio-stripe.html b/docs/architecture/benchmark-clio-stripe.html new file mode 100644 index 0000000..35d705c --- /dev/null +++ b/docs/architecture/benchmark-clio-stripe.html @@ -0,0 +1,445 @@ +Falah OS — Competitive Benchmark: Clio & Stripe + + +
+
+ Falah OS — Nur Falah Admin Console +

Competitive benchmark: Clio & Stripe Dashboard

+

Two reference products mapped against EstateOS and the cross-tenant super-admin surface. Every row ends in a call: steal the pattern, build it ourselves, or note it as a structural gap the reference product simply doesn't resolve.

+
+ +
+
+
+

Clio

+ vs. EstateOS +
+

Legal practice management — case files, billing, client portal, co-counsel collaboration.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Feature areaWhat Clio doesVerdict for EstateOS
Intake → billingClio Grow captures leads via custom forms, hands off to Clio Manage automatically once a matter opens.Steal
Document automationTemplate library with variable-filled document generation, version control.Steal
Billing & invoicingTimers, hourly/contingency billing, AI-drafted invoices with error flagging.Steal
Trust accountingClient trust ledgers, deposit/disbursement tracking, compliance reporting.Steal
Client portalEncrypted messaging, document sharing, payments — but single-brand, one firm per instance.Steal UX
Faraid / Syariah workflowsNo inheritance-share calculation, no Syariah Court filing templates — generic common-law matter types only.Build
Panel-lawyer vetting"Clio for Co-Counsel" assumes counsel is already chosen — it's a collaboration portal, not a credentialing tool.Build
Per-tenant brandingClient portal is single-brand per firm — no white-label capability for a platform serving multiple firms.Build
Multi-tenant backendClio is one firm, one instance — no shared-backend, multi-firm architecture exists to compare against.Structural gap
RAMZ row-level accessFirm-scoped permissions throughout — no concept of row-level, cross-tenant gating.Structural gap
+
+
+ +
+
+
+

Stripe Dashboard

+ vs. Falah OS super-admin +
+

Connect platform admin — connected-account management, roles, audit, API access.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Feature areaWhat Stripe doesVerdict for Falah OS
Admin role tiersAdmin vs. Super Administrator — Super Admin gates who can grant/revoke roles and manage environments.Steal
Audit loggingActivity Logs API — exportable, SOC 2/PCI-ready record of key changes, invitations, role changes.Steal
API key scopingRestricted keys limited to specific permission sets per integration.Steal
Tenant status viewConnected-account list with onboarding and risk status at a glance.Steal UX
Onboarding flowHosted or embedded onboarding, incremental — collects only what's currently required.Steal
Cross-regime compliance routingNo concept of routing tenants through different regulatory rule sets (Syariah Court vs. JKPTG vs. govt case routing).Build
Usage / billing-trigger eventsActivity Logs are security-focused, not usage-metering. Stripe's actual metering product wasn't covered — separate follow-up needed.Build / unclear
Tenant = product lineEvery connected account is a roughly uniform merchant entity — no support for managing distinct SaaS products per tenant.Structural gap
RAMZ row-level accessIsolation happens at the account level (own object graph per tenant) — closer to separate-DB than row-level gating.Structural gap
+
+
+ +
+ Steal pattern transfers directly + Build reference has nothing comparable + Structural gap reference's whole model doesn't apply +
+ +
+
+

Combined — steal list

+
    +
  1. Intake → billing handoff (Clio Grow → Manage)
  2. +
  3. Document automation + template library
  4. +
  5. Trust ledger compliance reporting
  6. +
  7. Two-tier Admin / Super Admin roles
  8. +
  9. Exportable audit log, scoped API keys
  10. +
+
+
+
+

Combined — build list

+
    +
  1. Faraid inheritance calculation engine
  2. +
  3. Panel-lawyer vetting / credentialing
  4. +
  5. Per-tenant white-labeled portal branding
  6. +
  7. RAMZ row-level, shared-schema isolation
  8. +
  9. Cross-regime compliance routing (Syariah / JKPTG / govt)
  10. +
+
+
+ + +
diff --git a/docs/architecture/integration-reality-check.html b/docs/architecture/integration-reality-check.html new file mode 100644 index 0000000..0ffba5f --- /dev/null +++ b/docs/architecture/integration-reality-check.html @@ -0,0 +1,259 @@ +Falah OS — Backend Integration Reality Check + + +
+
+ Falah OS — checked against the actual repos +

moslem03 has no backend either — same gap as moslem04

+

Verified against nur-muslim-companion-v2 and nur-falah-prevention 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.

+
+ +
+

01 · What moslem03 actually does today

+
+
+
+ moslem03.falahos.my + Svelte 5 PWA · static build +
+
→
+
+ Public third-party APIs + Al Quran Cloud · Aladhan · Overpass +
+
+
+
+ ✕ No auth + Anyone opens the app, no login +
+
+ ✕ No Falah OS server + Never calls anything on the Contabo VPS +
+
+ ✕ No admin console link + Nothing to sync — data never leaves the browser +
+
+
+ Same for moslem04 + Same localStorage-only pattern +
+
+
+ +
+

02 · What has to be built — bottom up

+
+
+
+
✓
+
+
+
casdoor (ummahid) identity service
+
Already running on the Contabo VPS :8000 — reuse it, don't rebuild login
+
+ Already exists +
+
+
+
1
+
+
+
Login screen in moslem04
+
App currently has zero auth UI — needs a casdoor sign-in flow bolted on
+
+ To build +
+
+
+
2
+
+
+
Sync API (new)
+
Push Faraid / Assets / Wassiyah / Hibah / Waqf drafts from localStorage to a server, encrypted
+
+ To build +
+
+
+
3
+
+
+
RAMZ tenant tagging
+
Every synced record gets stamped with which tenant/firm it belongs to
+
+ To build +
+
+
+
4
+
+
+
Admin console read view
+
Nur Falah Admin Console reads synced cases — view only, not edit
+
+ To build +
+
+
+
+ exists on the VPS today + needs to be built — nothing to copy from moslem03 +
+
+
+ + +
diff --git a/docs/architecture/ramz-tenant-isolation-decision.md b/docs/architecture/ramz-tenant-isolation-decision.md new file mode 100644 index 0000000..224773f --- /dev/null +++ b/docs/architecture/ramz-tenant-isolation-decision.md @@ -0,0 +1,67 @@ +# RAMZ Tenant Isolation — Decision + +**Status:** Resolved (revised 2026-08-18 — see amendment below) +**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 + +**Database-per-tenant on the shared Postgres instance.** + +Each tenant (EstateOS, Tanah Hidup, Cair) gets its own Postgres *database* — `falahestateos`, +`falahtanahhidup`, `falahcair` — not a schema, not a shared table filtered by a `tenant_id` +column. + +> **Amendment (2026-08-18):** this was originally decided as schema-per-tenant. Inspecting +> `falah-os-master/infrastructure/postgres/init.sql` showed the codebase already has an +> established convention — **database-per-service** (`falahid`, `falahwallet`, `falahramz`, +> `falahmock`, `falahistore`, all separate databases in one Postgres instance, not schemas). +> Database-per-tenant is a stronger isolation boundary than schema-per-tenant anyway, and it +> keeps one consistent pattern across the platform instead of introducing a second, competing +> one. Revised to match. + +## 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. +- Separate databases on one Postgres instance is the platform's existing convention, not a + new ops burden — `falah-os-master` already runs 5 databases this way. Adding 3 more + follows the grain of the system instead of cutting against it. + +## What RAMZ still does + +RAMZ doesn't disappear — it moves down a level. Within each tenant's database, RAMZ handles +the finer-grained access control that a database 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 database selector +(one connection pool per tenant), not a row filter. The `moslem-admin` service's +`/v1/admin/metadata` endpoint holds one Postgres pool per tenant database — there is no +shared table and no accidental cross-tenant query path, because a query issued against one +pool cannot physically reach another tenant's rows. + +## 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. diff --git a/docs/architecture/sync-api-spec.html b/docs/architecture/sync-api-spec.html new file mode 100644 index 0000000..b5d06e2 --- /dev/null +++ b/docs/architecture/sync-api-spec.html @@ -0,0 +1,224 @@ +moslem04 Sync API — Architecture Spec + + +
+
+ Falah OS — Nur Falah Estate & Waqf Suite +

Sync API architecture — moslem04.falahos.my

+

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.

+
Out of scope: moslem03 (Muslim Companion) stays exactly as-is — free, zero signup, no account, no sync, forever. This spec touches nothing there.
+
+ +
+

01Data flow, end to end

+
+
+
+ moslem04 (browser) + localStorage stays the source of truth offline; sync is additive, not a replacement +
+
→
+
+ casdoor (existing) + OIDC login, issues JWT — nothing new to build here +
+
→
+
+ Sync API (new) + Accepts encrypted blobs, tags with tenant + user, never decrypts +
+
→
+
+ Postgres (new) + Ciphertext + RAMZ tenant column + a small plaintext metadata row +
+
→
+
+ Admin console + Reads metadata only — opt-in, read-only, per Phase 2 below +
+
+
+
+ +
+

02What gets encrypted vs. what admin can see

+

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.

+
+ + + + + + + + + + +
FieldWho can read itNotes
payload_ciphertextUser onlyAES-GCM, key derived from casdoor session + on-device passphrase. Server stores bytes it cannot open.
moduleUser + adminWhich of the 6 modules the record belongs to — plaintext, needed for routing and counts only.
plan_completedUser + adminBoolean per module. Lets the console show "how many people finished their Wassiyah" without seeing a single will.
panel_lawyer_requestedUser + adminExplicit opt-in flag the user sets — the only trigger that surfaces a person to a panel lawyer at all.
tenant_id (RAMZ)Admin onlyWhich tenant/firm this record is scoped to — still pending the row-level isolation decision from the PRD review.
claims_payload (H2)Nobody, not syncedHorizon 2 Claims module is excluded from sync entirely until the Phase 0 legal gate in DEPLOY.md clears.
+
+
+ +
+

03Endpoints

+
+ + + + + + + + + + +
MethodPathPurpose
POST/v1/auth/exchangeTrade a casdoor OIDC token for a moslem04 session token
GET/v1/sync/pull?since=<ts>Fetch records changed since last sync — for restoring on a new device
POST/v1/sync/pushBatch upsert of encrypted blobs from the local device queue
DELETE/v1/sync/:record_idTombstone a record — deletes propagate across the user's own devices
POST/v1/consent/panel-lawyerThe one explicit opt-in action that makes a user visible to EstateOS admin at all
GET/v1/admin/metadataAdmin console read — returns only the plaintext metadata fields from §02, RAMZ-scoped
+
+
+ +
+

04Conflict handling

+

These are single-owner personal drafts, not shared documents — so last-write-wins by updated_at is sufficient for five of the six modules. Two exceptions: Family Waqf Designator can involve co-designators, so pushes there append to a change log instead of overwriting; Digital Claims (H2) doesn't sync at all, see §02.

+
+ +
+

05Rollout phases

+
+
+
+ Phase 0 + Auth + sync plumbing + Login, encrypted push/pull, cross-device restore. No admin visibility of any kind. +
+
+ Phase 1 + Opt-in metadata surface + Panel-lawyer request flag goes live. Admin sees only what §02 allows. +
+
+ Phase 2 · blocked + Full admin console read view + Waits on the RAMZ isolation decision and Core Ledger billing-event readiness — both still open from the earlier PRD review. +
+
+
+
+ + +
diff --git a/src/lib/FamilySwitcher.svelte b/src/lib/FamilySwitcher.svelte index 842c206..23412cc 100644 --- a/src/lib/FamilySwitcher.svelte +++ b/src/lib/FamilySwitcher.svelte @@ -6,6 +6,7 @@ let families = $state([]); let invites = $state([]); let newFamilyName = $state(''); + let searchQuery = $state(''); let loading = $state(true); let error = $state(''); @@ -45,6 +46,7 @@ } const isAgentForMultiple = $derived(families.filter(f => f.role === 'agent').length > 1); + const filteredFamilies = $derived(families.filter(f => f.name.toLowerCase().includes(searchQuery.toLowerCase())));
@@ -65,12 +67,22 @@ {#if loading}

Loading your families…

{:else} +
+

Start your own family estate

+

If you're the head of family setting this up for the first time, create your family here. You can invite an estate agent to help manage it once it's set up.

+ + +
+ {#if invites.length}

Pending invitations

{#each invites as inv} -
- {inv.familyName} — as {inv.role} +
+
+ {inv.familyName} + Role: {inv.role} +
{/each} @@ -79,22 +91,29 @@ {#if families.length}
-

{isAgentForMultiple ? 'Your assigned families' : 'Your families'}

- {#each families as f} - - {/each} +
+

{isAgentForMultiple ? 'Your assigned families' : 'Your families'}

+ +
+
+ {#each filteredFamilies as f} + + {/each} + {#if filteredFamilies.length === 0} +

No families found matching "{searchQuery}"

+ {/if} +
{/if} - -
-

Start your own family estate

-

If you're the head of family setting this up for the first time, create your family here. You can invite an estate agent to help manage it once it's set up.

- - -
{/if}
@@ -114,12 +133,30 @@ .hint { font-size: 11.5px; color: #8A8478; line-height: 1.5; margin-bottom: 12px; } .invite-row { display: flex; justify-content: space-between; align-items: center; background: rgba(201,168,76,0.08); border: 1px solid rgba(201,168,76,0.25); border-radius: 10px; padding: 12px; margin-bottom: 8px; font-size: 13px; color: #E8E4DC; } .btn-small { background: #C9A84C; color: #070A0D; border: none; border-radius: 8px; padding: 8px 12px; font-size: 12px; font-weight: 600; cursor: pointer; } - .family-row { width: 100%; display: flex; justify-content: space-between; align-items: center; background: rgba(255,255,255,0.04); border: none; border-radius: 10px; padding: 14px; margin-bottom: 8px; font-size: 14px; color: #E8E4DC; cursor: pointer; text-align: left; } + + .section-header { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; } + .search-input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 8px 12px; color: #E8E4DC; font-size: 13px; width: 100%; outline: none; transition: border-color 0.2s; } + .search-input:focus { border-color: #C9A84C; } + + .cards-grid { display: flex; flex-direction: column; gap: 10px; } + .family-card { width: 100%; display: flex; flex-direction: column; gap: 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 16px; font-size: 14px; color: #E8E4DC; cursor: pointer; text-align: left; transition: all 0.2s; } + .family-card:hover { background: rgba(255,255,255,0.06); border-color: rgba(201,168,76,0.3); transform: translateY(-1px); } + .card-top { display: flex; justify-content: space-between; align-items: center; width: 100%; } + .card-bottom { display: flex; justify-content: space-between; align-items: center; width: 100%; font-size: 11px; color: #8A8478; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px; } + .card-title { font-weight: 600; font-size: 15px; } + .status-indicator { display: flex; align-items: center; gap: 4px; } + .status-indicator::before { content: ''; width: 6px; height: 6px; background: #2ECC71; border-radius: 50%; } + .action-hint { color: #C9A84C; opacity: 0; transition: opacity 0.2s; } + .family-card:hover .action-hint { opacity: 1; } + + .empty-state { text-align: center; color: #8A8478; font-size: 13px; padding: 20px 0; font-style: italic; } + .role-badge { font-size: 10px; text-transform: uppercase; padding: 3px 8px; border-radius: 6px; background: rgba(255,255,255,0.1); color: #B8B2A6; } .role-badge.owner { background: rgba(201,168,76,0.15); color: #C9A84C; } .field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } .field span { font-size: 12px; color: #B8B2A6; } .field input { background: rgba(255,255,255,0.05); border: 1px solid rgba(201,168,76,0.2); border-radius: 8px; padding: 10px 12px; color: #E8E4DC; font-size: 14px; width: 100%; } - .btn-primary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; background: #C9A84C; color: #070A0D; } + .btn-primary { width: 100%; padding: 12px; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; background: #C9A84C; color: #070A0D; transition: opacity 0.2s; } + .btn-primary:hover { opacity: 0.9; } .error-text { color: #EF4444; font-size: 12.5px; margin-bottom: 12px; }