Legal aid for a district office in Bangladesh, from the first phone call to the officer’s decision.
A person calls a hotline and an AI listens, verifies who they are against the National ID registry, and files the case. The District Legal Aid Officer (DLAO) confirms the AI’s marks, both sides hear by SMS, courts and jails apply on behalf of people before them and Union Digital Centres on behalf of neighbours who cannot use the forms at all, a panel lawyer takes the case to court and reports back, and every step is audited.
Contents
| Part | What it is | Stack | Port |
|---|---|---|---|
server/ |
The backend: AI hotline intake by phone (live speech-to-text), triage, SMS notices, the AI query helpline, and the officer API | FastAPI, SQLAlchemy 2, LangGraph, SQLite (Postgres in production) | 8000 |
nid-server/ |
A National ID registry with fictional citizens, parent links and registered SIMs | FastAPI, rapidfuzz, read-only in-memory data | 8100 |
dlao-dashboard/ |
The District Legal Aid Officer’s dashboard (English and বাংলা) | React 19, TypeScript, Vite, Tailwind v4, shadcn/ui | 5173 |
lawyer-dashboard/ |
The panel lawyers’ dashboard: their cases and hearings, updates from court, each case’s court record, and the bill for a case that has closed (English and বাংলা) | same as above | 5174 |
court-dashboard/ |
The courts’ dashboard: the court’s register and cause lists, legal aid applications for people before the court with e-KYC and e-signature, and panel lawyers’ bills to check and release (English and বাংলা) | same as above | 5175 |
prison-dashboard/ |
The jails’ dashboard: prisoners and the court cases they are held on, the production list, and legal aid applications for prisoners, with e-KYC and e-signature (English and বাংলা) | same as above | 5176 |
udc-dashboard/ |
The Union Digital Centres’ dashboard: filing an application for a neighbour who cannot use the forms, the papers they brought, and mediation dates to pass on in person (English and বাংলা) | same as above | 5177 |
portal/ |
The front page: the hotline number, and a button for each app with the address it opens (English and বাংলা) | one static HTML file | any static server |
user-mobile-app/ |
The citizen’s Android app: SOS (both volume buttons call for help, from any screen), filing a case, their cases and each one’s progress, documents, the helpline and mediation notices (English and বাংলা) | Expo SDK 57, React Native 0.86, a Kotlin module for SOS | Metro 8081 |
External services the backend can use (all optional in development; without them the system falls back to rules, dry-run SMS and a spoken fallback message):
| Service | Used for |
|---|---|
| Twilio | Phone lines: voice webhooks and a media stream carrying the call’s audio |
OpenAI gpt-live-transcribe |
Live speech-to-text for callers (Bangla and English) |
ElevenLabs (eleven_v3) |
The line’s spoken replies, in Bangla or English |
Claude or OpenAI (LLM_PROVIDER) |
Optional model consulted where rules are weak (triage, intake, documents, settlement drafts, helpline) |
| ADN SMS | Notices to filers, respondents, mediation parties and Union Digital Centres |
| ngrok | Public tunnel so Twilio can reach a laptop (local development only) |
Who uses which dashboard, and how each one signs in to the backend:
| Person | Dashboard | API prefix | Identity header |
|---|---|---|---|
| District Legal Aid Officer | dlao-dashboard |
/dlao/*, /mediation/*, /duplicates, /referrals, /incidents |
X-Officer-Id |
| Panel lawyer | lawyer-dashboard |
/lawyer/*, /lawyer/bills/* |
X-Lawyer-Id |
| Court staff (bench assistant, sheristadar) | court-dashboard |
/court/*, /court/bills/* |
X-Court-Staff-Id |
| Jail staff (legal aid desk, deputy jailer) | prison-dashboard |
/prison/* |
X-Prison-Staff-Id |
| Union Digital Centre entrepreneur | udc-dashboard |
/udc/* |
X-Udc-Id |
| Caller | the phone (or /intake/* and /helpline/* on the web) |
/telephony/* |
none (Twilio signature) |
| Citizen (or someone applying for them) | user-mobile-app (Android) |
/intake/web, /intake/cases/{ref}/documents, /helpline/* |
none: a case is theirs by its tracking number |
Everything meets at the backend. The dashboards never talk to each other: what a court saves reaches a jail, the officer and the lawyer because they all read the same records from the server.
flowchart TB
subgraph Front["Front ends (React, English and বাংলা)"]
direction LR
DLAO["dlao-dashboard :5173<br/>DLAO officer"]
LAW["lawyer-dashboard :5174<br/>panel lawyer"]
COURT["court-dashboard :5175<br/>court staff"]
PRISON["prison-dashboard :5176<br/>jail staff"]
UDC["udc-dashboard :5177<br/>Union Digital Centre"]
end
Caller(["Caller<br/>hotline or helpline"])
subgraph Back["server/ (FastAPI, :8000)"]
direction TB
Routers["Routers<br/>intake, dlao, records, lawyer, court, prison,<br/>duplicates, referrals, incidents, mediation,<br/>udc, sync, helpline, telephony"]
Agents["AI agents (LangGraph)<br/>T5 intake, T6 documents, T7 settlement,<br/>T8 triage, helpline, hotline menu"]
Services["Services<br/>safe_contact, notices, ekyc, records,<br/>institution, mediation, case_status,<br/>court_progress, rosters"]
Voice["Voice pipeline<br/>stream_manager, audio,<br/>speech_to_text, elevenlabs"]
DB[("SQLite / Postgres<br/>cases, parties, records,<br/>mediation, documents")]
Audit[("Audit ledger<br/>SHA-256 hash chain")]
Routers --> Agents
Routers --> Services
Routers --> Voice
Agents --> Services
Services --> DB
Services --> Audit
end
NID["nid-server :8100<br/>National ID registry<br/>(fictional citizens)"]
subgraph Ext["External services"]
direction LR
Twilio["Twilio<br/>voice + media stream"]
STT["OpenAI<br/>gpt-live-transcribe"]
TTS["ElevenLabs<br/>text-to-speech"]
LLM["Claude or OpenAI<br/>optional model"]
ADN["ADN SMS"]
end
DLAO -->|"/dlao, /mediation"| Routers
LAW -->|"/lawyer"| Routers
COURT -->|"/court"| Routers
PRISON -->|"/prison"| Routers
UDC -->|"/udc, /sync"| Routers
Caller <-->|"phone call"| Twilio
Twilio <-->|"webhooks + audio (via ngrok)"| Voice
Voice -->|"caller audio"| STT
Voice -->|"reply text"| TTS
Agents -.->|"where rules are weak"| LLM
Services -->|"verify, family, SIM lookup"| NID
Services -->|"SMS"| ADN
ADN -.->|"SMS to filers, respondents,<br/>parties and UDCs"| Caller
How to read it:
POST /sync/batch).safe_contact before it reaches ADN, so a blocked or
restricted person is never texted by mistake.Who writes what, and who reads it. This is what “reaches the others at once” means.
flowchart LR
Hotline["Hotline call<br/>(T5 intake)"] -->|files application,<br/>triage marks, notices| Cases[("Cases and parties")]
CourtDash["court-dashboard"] -->|register, hearings,<br/>cause lists| CourtRecs[("Court records")]
CourtDash -->|application + e-KYC<br/>+ e-signature| Cases
JailDash["prison-dashboard"] -->|prisoners and the<br/>cases they are held on| PrisonRecs[("Prisoner records")]
JailDash -->|application + e-KYC<br/>+ e-signature| Cases
OfficerDash["dlao-dashboard"] -->|confirm triage, assign lawyer,<br/>schedule mediation, link records| Cases
LawyerDash["lawyer-dashboard"] -->|updates from court,<br/>order sheets| Progress[("Court progress<br/>reports")]
CourtRecs <-->|linked by case number| PrisonRecs
CourtRecs -->|linked to a case| Cases
PrisonRecs -->|linked to a case| Cases
Progress --> Cases
Cases -->|queue, alerts, records,<br/>court progress, mediation| OfficerDash
Cases -->|own cases + court record| LawyerDash
Cases -->|stage, lawyer, next hearing| CourtDash
Cases -->|stage, lawyer, next hearing| JailDash
PrisonRecs -->|production list<br/>from cause lists| JailDash
Cases -->|progress by tracking number| Helpline["AI helpline"]
server/: the backendRequests pass through three layers. Routers check who is asking and shape the response, services hold the rules, and models are the tables. Agents are LangGraph graphs that run without any model and consult one only where rules are weak.
flowchart TB
Client["Dashboards, Twilio, UDC tablets, /docs"] --> Auth
subgraph API["routers/"]
Auth["Auth: bearer API_TOKEN<br/>+ X-Officer-Id, X-Lawyer-Id,<br/>X-Court-Staff-Id, X-Prison-Staff-Id, X-Udc-Id"]
Auth --> R1["intake, helpline, telephony<br/>(caller-facing)"]
Auth --> R2["dlao, records, duplicates,<br/>referrals, incidents, mediation<br/>(officer)"]
Auth --> R3["lawyer, court, prison, udc<br/>(each scoped to its own records)"]
Auth --> R4["sync<br/>(offline batches)"]
end
subgraph AG["agents/"]
T5["T5 intake"]
T6["T6 documents"]
T7["T7 settlement"]
T8["T8 triage"]
HL["helpline + hotline_menu"]
LLMC["llm.py<br/>optional Claude / OpenAI"]
end
subgraph SV["services/"]
SC["safe_contact"]
NT["notices, mediation"]
EK["ekyc, institution"]
RC["records, courts, prisons,<br/>court_progress, case_status"]
NR["nid_registry"]
SMS["adnsms"]
CR["crypto (HMAC, Ed25519)"]
VP["stream_manager, audio,<br/>speech_to_text, elevenlabs"]
end
subgraph MD["models/"]
M1["case, party"]
M2["records"]
M3["mediation, lawyer, document"]
M4["audit"]
end
R1 --> T5
R1 --> HL
R1 --> VP
R2 --> T6
R2 --> T7
R2 --> T8
T5 --> T8
R3 --> EK
R3 --> RC
R2 --> NT
R4 --> T5
T5 -.-> LLMC
T6 -.-> LLMC
T7 -.-> LLMC
T8 -.-> LLMC
HL -.-> LLMC
T5 --> NR
EK --> NR
NT --> SC
T5 --> SC
SC --> SMS
SV --> MD
SC --> M4
| Layer | Contents |
|---|---|
routers/ |
intake, dlao, records (the officer’s view of court and jail records), lawyer, court, prison, duplicates, referrals, incidents, mediation, udc, sync, helpline, telephony |
agents/ |
t5_intake, t6_document, t7_settlement, t8_triage, helpline, hotline_menu, plus llm (optional model access), spoken (reading callers’ answers) and state |
services/ |
safe_contact, notices, mediation, adnsms, nid_registry, ekyc, institution, records, courts, prisons, panel, udc, case_status, court_progress, uploads, crypto, and the voice services |
models/ |
case, party, document, lawyer, records, mediation, audit |
| Feature | Where | |
|---|---|---|
| T1 | Alerts: overdue, lawyer inactivity, jurisdiction escalation, untriaged critical cases | GET /dlao/alerts |
| T2 | Referrals between district offices; ping-pong is escalated, not forwarded | /referrals |
| T3 | Group incidents linking cases from one event, with suggestions | /incidents |
| T4 | Fuzzy duplicate detection on every intake; merge blocked when NIDs differ | /duplicates |
| T5 | Conversational intake for the hotline, UDC and web | /intake/conversations, telephony |
| T6 | Document reading (OCR), summaries and a missing-documents checklist | POST /intake/cases/{ref}/documents |
| T7 | Settlement drafting under Section 21C, LASA 2000 | POST /mediation/cases/{ref}/settlement-draft |
| T8 | Triage: categorization, compliance, urgency, track | runs on every intake; /dlao/cases/{ref}/triage/* |
| T9 | Idempotent offline batch sync for the PWA | POST /sync/batch |
| T11 | Ed25519 e-signatures on approved settlements | POST /mediation/signatures |
The full endpoint list, each rule and every known limitation are in
server/README.md.
Each agent is a LangGraph graph. Any model failure (network, rate limit, refusal, invalid output) falls back to rules, and requests use structured outputs.
flowchart TB
subgraph T5g["T5 intake: one turn of a conversation"]
direction LR
e1["extract<br/>read the caller's answer"] --> e2["check_danger<br/>999 / hostage / violence"]
e2 --> e3["listen<br/>does it sound like a case?"]
e3 -->|"nothing to act on"| eEnd(["end call"])
e3 -->|"a case"| e4["resolve<br/>who, NID questions, relative,<br/>respondent, contact"]
e4 --> e5["respond<br/>next question or file it"]
end
subgraph T8g["T8 triage: runs on every application"]
direction LR
t1["categorize<br/>keyword rules, model for<br/>unclear narratives"] --> t2["compliance<br/>risk factors and warning signs,<br/>each with the evidence found"]
t2 --> t3["urgency<br/>rule-based priority"]
t3 --> t4["track<br/>advice, mediation or sensitive"]
end
subgraph T6g["T6 documents"]
direction LR
d1["read<br/>OCR scans and PDFs"] --> d2["checklist<br/>missing documents"]
end
subgraph T7g["T7 settlement draft"]
direction LR
s1["screen<br/>refuse if violence on record"] -->|"ok"| s2["draft<br/>Section 21C, LASA 2000"]
s1 -->|"stop"| sEnd(["stop"])
s2 --> s3["review<br/>a draft that drops a term,<br/>party or statute is replaced<br/>by the template"]
end
subgraph HLg["Helpline"]
direction LR
h0["hotline_menu<br/>new case or status?"] --> h1["answer<br/>tracking number, notice number,<br/>office, documents, mediation"]
end
| Agent | Rules only | With a model configured |
|---|---|---|
| T5 intake | Keyword and pattern rules for problems, danger, names, phone numbers, dates | Reads every turn alongside the rules for free speech; can add a “this is a case” or “not a legal matter” verdict, never turn away a problem the rules know |
| T6 documents | Checklist by document kind | Reads scans and PDFs (image and PDF blocks) |
| T7 settlement | Template draft | Drafts, then review rejects a draft that drops a term, a party or the statutory reference |
| T8 triage | Keyword categorization, transparent priority rules | Categorizes narratives the rules cannot; may choose between advice and mediation but never lowers a sensitive mark; priority stays rule-based |
| Helpline | Fixed facts about the office, documents, fees and mediation | Answers other questions from the same facts |
One call is one Twilio media stream. The backend listens, thinks and speaks on the same socket, and the caller can talk over the line at any time.
sequenceDiagram
autonumber
actor C as Caller
participant TW as Twilio
participant TP as telephony router
participant SM as stream_manager
participant AU as audio.py
participant ST as gpt-live-transcribe
participant AG as Agent (menu, T5 or helpline)
participant EL as ElevenLabs
C->>TW: dials the hotline or helpline number
TW->>TP: POST /telephony/voice (signature checked)
TP-->>TW: TwiML: open a media stream
TW->>SM: WebSocket, μ-law 8 kHz audio
SM->>EL: greeting text
EL-->>SM: speech (streamed over HTTP)
SM-->>TW: audio (first 0.6 s held back, sped up 1.2x)
TW-->>C: hears the greeting
loop each turn
C->>TW: speaks
TW->>SM: audio frames
SM->>AU: decode, upsample to 24 kHz, detect voice
Note over AU: turn starts after 200 ms of speech,<br/>ends after 700 ms of quiet<br/>(1200 ms while the caller tells what happened)
AU->>ST: the turn's audio only
ST-->>SM: transcript
SM->>AG: transcript
AG-->>SM: reply text, or file the case
SM->>EL: reply text
EL-->>SM: speech
SM-->>TW: audio
TW-->>C: hears the reply
C-->>SM: talks over the line (barge-in stops playback)
end
TW->>TP: POST /telephony/status (call ended or cut)
TP->>AG: a cut call is filed, do-not-call if violence was described
Failure paths: a lost speech-to-text session is reopened up to three times (after 0.3, 1 and
2 s); with no OPENAI_API_KEY the caller hears a short message (call 999 if in danger, apply
at a UDC) and the call ends; a voice that is definitely broken gets Twilio’s own spoken
“cannot take applications by phone” message rather than silence.
flowchart TB
In1["Hotline / web<br/>T5 intake"] --> New
In2["court-dashboard<br/>POST /court/applications"] --> Inst
In3["prison-dashboard<br/>POST /prison/applications"] --> Inst
In4["udc-dashboard<br/>POST /udc/applications"] --> Inst
In5["UDC tablet, offline<br/>POST /sync/batch"] --> New
Inst["institution service<br/>verified e-KYC used once,<br/>registry's details win,<br/>client_ref makes a retry safe"] --> New
New["Create application<br/>APP-year-number + 8-digit tracking number"] --> Dup["T4 duplicate check<br/>(merge blocked if NIDs differ)"]
Dup --> Tri["T8 triage<br/>priority + advice / mediation / sensitive"]
Tri --> Link["Link court case or prisoner<br/>by normalised case number"]
Link --> Q["DLAO queue and alerts"]
Tri --> Notice{"Can the applicant<br/>be reached?"}
Notice -->|"a phone number is known"| SMS["SMS notices through safe_contact:<br/>tracking number to the filer,<br/>visit-the-office to the respondent<br/>(held for an officer if risky)"]
Notice -->|"in the dock, in a cell,<br/>or no phone at all"| Hand["No SMS: staff or the centre<br/>hand the tracking number over"]
Tri --> Aud[("Audit ledger")]
Nothing reaches a phone except through safe_contact. Every decision, sent or blocked, is
audited without the message body.
flowchart TB
Msg["Any outbound SMS or call"] --> DNC{"Do-not-call?<br/>safety level no_contact"}
DNC -->|"yes"| Block["Blocked and audited"]
DNC -->|"no"| Hold{"Notice held?<br/>caller did not agree, sensitive,<br/>emergency, or identity unverified"}
Hold -->|"yes"| Wait["Held for an officer<br/>(release needs a written reason)"]
Hold -->|"no"| Lvl{"Party's safety level"}
Lvl -->|"standard"| Send["Send the full text"]
Lvl -->|"caution or<br/>shared phone"| Neutral["Send a neutral text only<br/>(no case details)"]
Lvl -->|"restricted"| Win{"Inside the weekly<br/>safe window?"}
Win -->|"no"| Block
Win -->|"yes"| Neutral
Send --> Gate{"SMS_DRY_RUN or<br/>SMS_ALLOWLIST"}
Neutral --> Gate
Gate -->|"dry run, or not on the list"| Log["Logged, not sent"]
Gate -->|"live"| ADN["ADN SMS"]
Send --> Aud[("Audit ledger")]
Neutral --> Aud
Block --> Aud
Wait --> Aud
flowchart TB
Sch["Officer schedules a session<br/>POST /mediation/sessions"] --> N["Notice to each party by SMS<br/>date, place, 8-digit notice number,<br/>helpline number (through safe_contact)"]
N --> Held{"Do-not-call or<br/>sensitive case?"}
Held -->|"yes"| HN["Notices held"]
Held -->|"no"| Sent["Sent"]
Sent --> Call["Party calls the helpline and says<br/>the notice number"]
Call --> Exp["Helpline explains the notice:<br/>when, where, what to bring"]
Sent --> Att["Officer records who came<br/>POST /mediation/sessions/id/attendance"]
Att --> Miss{"Anyone absent?"}
Miss -->|"yes"| Ms["Session is missed"]
Ms --> Lim{"Missed 2 in a row?<br/>MEDIATION_NO_SHOW_LIMIT"}
Lim -->|"yes"| Flag["Case flagged mediationNoShow"]
Flag --> Risk{"Applicant at risk,<br/>sensitive or do-not-call?"}
Risk -->|"yes"| UH["UDC notice held<br/>(officer can release with a reason)"]
Risk -->|"no"| UN["UDC of the party's upazila gets the<br/>next date and place by SMS"]
UN --> UI["UDC tells the person in person<br/>POST /udc/notices/id/informed"]
Miss -->|"no"| Ok["Session held"]
Ok --> Draft["Optional: T7 settlement draft,<br/>officer approves, parties sign (Ed25519)"]
nid-server/: the National ID registryA read-only stand-in for the Election Commission’s verification service, holding 40 fictional citizens. The backend uses it to verify callers, find relatives and find a respondent’s SIMs.
flowchart LR
subgraph Callers["Called by server/ only"]
A["T5 intake<br/>verify the caller"]
B["T5 intake<br/>relative or respondent"]
C["T5 intake<br/>SIM fallback, respondent SIMs"]
D["e-KYC from the<br/>court and jail dashboards"]
end
subgraph NIDS["nid-server :8100"]
K["X-API-Key check<br/>(open if NID_API_KEY is empty)"]
K --> E1["POST /v1/citizens/match"]
K --> E2["GET /v1/citizens/nid"]
K --> E3["GET /v1/citizens/nid/family"]
K --> E4["GET /v1/sims/msisdn"]
E1 --> REG["registry.py<br/>normalise, fuzzy match"]
E2 --> REG
E3 --> REG
E4 --> REG
REG --> DATA[("citizens.json<br/>loaded once at startup,<br/>integrity-checked")]
end
A --> E1
B --> E3
C --> E4
D --> E2
Matching rules in short: a request needs a name plus at least two other details; every given
field must agree; names are compared fuzzily (rapidfuzz token_sort_ratio, threshold 85,
honorifics removed, English or Bangla spelling); date of birth must be exact; districts
ignore old spellings (Chittagong, Comilla, Bogra and so on). unique is true only when
exactly one citizen matched. The service refuses to start if the data file breaks its
integrity rules. See nid-server/README.md.
The registry is also used for the e-KYC check that courts and jails run before a signature is accepted:
sequenceDiagram
autonumber
actor S as Court or jail staff
participant D as Dashboard
participant API as server (/court or /prison)
participant N as nid-server
S->>D: NID, date of birth, name (optional)
D->>API: POST /court/ekyc (or /prison/ekyc)
API->>N: GET /v1/citizens/{nid}
N-->>API: the citizen, or 404
alt matches
API-->>D: verified, the registry's details, check id
Note over API: kept for one use, by this office,<br/>within EKYC_CHECK_VALID_MINUTES
else does not match
API-->>D: did not match (never says which detail)
Note over D: after two misses, staff may send without e-KYC<br/>and verify later from the application page
end
S->>D: applicant signs on screen or a scan is uploaded
D->>API: POST /applications with the check id and signature
API-->>D: tracking number
All four are the same kind of app: React 19, TypeScript, Vite, Tailwind CSS v4, shadcn/ui,
hash URLs (so dist/ works on any static host), and a whole UI in English and বাংলা. They
share one shape:
flowchart TB
Main["main.tsx<br/>providers + hash router"] --> Guard["auth/<br/>sign-in (session storage)<br/>+ route guard"]
Guard --> Pages["pages/<br/>one file per screen"]
Pages --> State["state/<br/>reducers and providers"]
Pages --> I18N["i18n/<br/>en + bn dictionaries,<br/>formatters (Anek Bangla font)"]
Pages --> UI["components/<br/>ui (shadcn), layout, feature parts"]
State --> API["api/<br/>backend client:<br/>sends bearer token + identity header,<br/>maps server views to the UI's types"]
API --> Env{"VITE_API_URL set?"}
Env -->|"yes"| Server["server/ :8000<br/>live records"]
Env -->|"no"| Sample["data/<br/>built-in sample records<br/>(in memory, reset on reload)"]
Pages --> Lib["lib/<br/>rules mirrored from the backend<br/>(safe-contact window, court progress, record search)"]
Without VITE_API_URL each dashboard runs on built-in sample data, so it can be tried with
no backend. With it, the backend’s CORS_ORIGINS must include the dashboard’s port.
dlao-dashboard/ (port 5173): the officerSign in with any officer ID and a password of 4+ characters (a demo account is one click). Opening a case fetches its history, call notes, the lawyer’s reports and transfers; the Court and jail records and Mediation tabs are fetched only when opened, because the server audits every look.
flowchart TB
SI["Sign in"] --> Home["Home<br/>summary numbers, pattern alerts,<br/>'Start here', upcoming hearings"]
Home --> Queue["Work queue<br/>New, Urgent, Overdue; filters and search"]
Home --> Lawyers["Lawyers<br/>open cases, who stopped reporting"]
Home --> Hearings["Hearings<br/>court dates + mediation, next 2 weeks"]
Queue --> Case
Lawyers --> Reassign["Review and Reassign<br/>move cases to another lawyer"]
Hearings --> Case
Home --> Others["All cases, Reports, Profile,<br/>Settings, Help, Notifications"]
subgraph Case["Case (dialog, full screen on a phone)"]
direction TB
C1["Safety warning + what to do now"]
C2["AI triage: accept or override<br/>(20+ character reason)"]
C3["Advice / mediation / sensitive mark:<br/>confirm or change"]
C4["Case information, respondent SMS<br/>(send if held), sensitive evidence (Role B6)"]
C5["Court and jail records tab<br/>view, search, link a record"]
C6["Court progress<br/>lawyer reports, remind"]
C7["Mediation tab<br/>schedule, attendance, UDC notices"]
end
Queue --> Dup["Duplicate check<br/>compare, confirm distinct (merge blocked)"]
Case --> API["/dlao/*, /mediation/*, /lawyer reports"]
Saved on the server: accepting or overriding triage, the advice / mediation / sensitive mark, sending a held SMS, assigning or moving a lawyer, reminding a lawyer, escalating to the Chief Legal Aid Officer, acknowledging sensitive evidence, linking a court case or prisoner, scheduling mediation, recording attendance, and sending a held UDC notice. Safe-call booking, duplicate decisions, marking a late task done and hearing reminders stay on screen only for now.
lawyer-dashboard/ (port 5174): the panel lawyerA lawyer sees only the cases an officer assigned to them. The server sends no phone number for an applicant nobody may call, and shows the safe time for a watched phone.
flowchart TB
SI["Sign in<br/>GET /lawyer/me checks the ID"] --> My["My cases<br/>profile, four numbers, 'office is waiting'<br/>when a report is late"]
My --> Case["Case<br/>court progress, client and safe contact,<br/>complaint against whom"]
My --> Hear["Hearings<br/>past dates with no report,<br/>then the next 30 days"]
Case --> Rec["Court record<br/>GET /lawyer/cases/ref/records<br/>(fetched on open, audited, no NID digits)"]
Case --> Upd
Hear --> Upd["Send an update (dialog)<br/>stage, court, hearing date, next date,<br/>what happened (20+ chars), order sheet up to 10 MB"]
Upd --> Srv["server: stores the report,<br/>next hearing = last date fixed"]
Srv --> Off["Officer sees it at once<br/>(Court progress tab)"]
Srv --> Rule{"Report every 14 days<br/>and within 3 days of a hearing?"}
Rule -->|"missed"| Late["Reports late, then<br/>lawyer inactivity alert,<br/>then pattern alert across cases"]
court-dashboard/ (port 5175): court staffA court sees only its own register, cause lists and applications. Anything else is “not found”, so another court’s record IDs are never confirmed.
flowchart TB
SI["Sign in<br/>GET /court/me"] --> Today["Today<br/>today's cause list, applications<br/>waiting for e-KYC or a signature"]
Today --> CL["Cause list /cause-lists/date<br/>edit rows, paste from a spreadsheet,<br/>saving an empty list withdraws it"]
Today --> Cases["Cases /cases<br/>the register"]
Cases --> Reg["Register a case<br/>parties, sections, restricted flag"]
Cases --> Case["Case /cases/id<br/>parties, proceedings, lawyers,<br/>who is held and where"]
Case --> Proc["Record proceedings<br/>judgment disposes the case"]
Case --> Lw["Add lawyer, End appearance"]
Case --> Apply["Apply for legal aid<br/>(from a party)"]
Today --> Apply
subgraph Wiz["New application: four steps"]
direction LR
W1["1. e-KYC<br/>NID, date of birth, name"] --> W2["2. The application"] --> W3["3. Signature<br/>draw or scan, only once verified"] --> W4["4. Review and submit<br/>one client_ref, no double filing"]
end
Apply --> Wiz
W4 --> AppList["Legal aid /applications<br/>stage, lawyer, next hearing"]
AppList --> App["Application /applications/ref<br/>tracking number to hand over,<br/>Verify now, Add signature"]
prison-dashboard/ (port 5176): jail staffA jail sees only its own prisoners and applications, and of each court case only what it needs to produce the prisoner. It never sees the case file, proceedings, other parties, or what the officer does inside a case beyond stage, lawyer and next hearing.
flowchart TB
SI["Sign in<br/>GET /prison/me"] --> Today["Today<br/>prisoners to produce today and tomorrow,<br/>undertrial prisoners with no application yet"]
Today --> CD["Court dates /court-dates<br/>production list by date then court,<br/>Print"]
Today --> Pr["Prisoners /prisoners<br/>search, filter by status"]
Pr --> Adm["Admit a prisoner<br/>optional e-KYC, court cases held on"]
Pr --> P["Prisoner /prisoners/id<br/>details, each court case<br/>(registered by the court or not yet)"]
P --> Apply["Apply for legal aid"]
Today --> Apply
subgraph Wiz["New application: four steps"]
direction LR
W1["1. e-KYC<br/>prefilled from the prisoner"] --> W2["2. The application"] --> W3["3. Signature<br/>only once verified"] --> W4["4. Review and submit"]
end
Apply --> Wiz
W4 --> Apps["Legal aid /applications"]
Apps --> App["Application /applications/ref<br/>tracking number for the prisoner or family,<br/>panel lawyer, next hearing"]
CD -.->|"cause lists saved by the court"| CourtSide["court-dashboard"]
udc-dashboard/ (port 5177): Union Digital Centre entrepreneursThe channel that reaches the people the others cannot. A court can only help someone already before it, the hotline needs a phone the caller can speak on privately, and the Android app needs a smartphone and the confidence to use it. A centre needs none of that: the applicant walks into the union’s centre, the entrepreneur does the typing and scans the papers they brought, and they leave with a tracking number.
A centre sees only its own applications and its own mediation notices. It has no records of its own to link, cannot name a court case or a prisoner, and cannot mark anyone as in custody — someone in custody applies through the court or jail holding them.
flowchart TB
SI["Sign in<br/>GET /udc/me"] --> Today["Today<br/>dates still to pass on,<br/>applications only the centre can finish"]
Today --> File["File an application"]
subgraph Wiz["File an application: five steps"]
direction LR
W1["1. e-KYC<br/>NID and date of birth<br/>from their card"] --> W2["2. The applicant<br/>phone, and why the<br/>centre is filing"] --> W3["3. Papers<br/>NID, kabinnama, porcha,<br/>a GD copy"] --> W4["4. Signature<br/>finger or thumbprint,<br/>only once verified"] --> W5["5. Read it back<br/>and file"]
end
File --> Wiz
W5 --> Done["Tracking number<br/>read out, printed, and by SMS<br/>when a number was given"]
Done --> Apps["Applications /applications"]
Apps --> App["Application /applications/ref<br/>stage, lawyer, next hearing,<br/>Check identity now, Take their signature,<br/>Add a paper"]
Today --> Notices["Mediation dates /notices<br/>who to tell, where they live,<br/>the date and place"]
Notices --> Told["I told them<br/>POST /udc/notices/id/informed"]
user-mobile-app/: the citizen’s appAn Android app for the people the office serves, in Bangla or English. It has no account: a
case is the person’s because they hold its tracking number, which the backend answers with
the stage and the next dates only. What the app keeps stays on the phone. Details, and how
to test SOS on an emulator, are in user-mobile-app/README.md.
| Screen | What it does | Backend |
|---|---|---|
| File a case | Five steps (who, about them, what happened, safety, check); waits on the phone when offline | POST /intake/web |
| My cases, progress | Cases filed here or added by tracking number; the steps, next hearing or mediation, read aloud | GET /helpline/track/{token} |
| Documents | Photos or PDFs, and the office’s checklist of missing papers | POST /intake/cases/{ref}/documents |
| Help | 16430 and 999, the AI helpline chat, mediation notice numbers | /helpline/* |
| SOS | Volume up + volume down together calls +19788458907 | none (the phone) |
SOS is native code (a local Expo module, modules/sos-gesture). An accessibility service
hears the two volume keys over any app and on the lock screen. A foreground service keeps SOS
armed after the app is closed or the phone restarts, and places the call through Android’s
Telecom. With the screen off, Android 13 and newer give the volume keys to no app, so there
the power button is pressed once first.
sequenceDiagram
autonumber
actor Caller
participant Line as Hotline (T5)
participant NID as NID registry
participant Srv as Backend (T8, notices)
participant DL as DLAO dashboard
participant SMS as ADN SMS
actor Resp as Respondent
participant Law as Lawyer dashboard
Caller->>Line: calls, says what happened
Line->>NID: verify by three security questions (or SIM fallback)
NID-->>Line: one matching record, or unverified
Line->>Srv: file the application (APP-…)
Srv->>Srv: T4 duplicate check, T8 triage (priority + mark)
Srv->>SMS: tracking number to the filer (via safe_contact)
Srv->>SMS: visit-the-office notice to the respondent (held if risky)
SMS-->>Resp: notice with the helpline number
DL->>Srv: confirm or override triage and the mark (with a reason)
DL->>Srv: accept as a case (gains DLAS-… number)
alt mediation
DL->>Srv: schedule a session
Srv->>SMS: notices to both parties
DL->>Srv: record who came
end
DL->>Srv: assign a panel lawyer
loop every 14 days and within 3 days of each hearing
Law->>Srv: update from court (stage, next date, order sheet)
Srv-->>DL: report visible at once, late ones raise alerts
end
DL->>Srv: close the case with an outcome
A case’s status and the separate marks an officer confirms. The AI only marks; a human
decides.
stateDiagram-v2
[*] --> application: filed (hotline, UDC, web, court, jail)
application --> active: officer accepts, gains DLAS number
application --> referred: sent to another district office
active --> referred: referral
referred --> application: returned, no case number yet
referred --> active: returned, has a case number
active --> in_mediation: a session is scheduled
application --> in_mediation: a session is scheduled
in_mediation --> closed: officer closes with an outcome
active --> closed: officer closes with an outcome
closed --> [*]
note right of closed
Outcome: resolved, settled,
withdrawn or referred
end note
stateDiagram-v2
direction LR
state "Triage" as tri {
[*] --> pending: T8 runs on intake
pending --> accepted: officer accepts
pending --> overridden: officer overrides (20+ character reason)
}
state "Advice / mediation / sensitive mark" as trk {
[*] --> suggested: T8 marks it
suggested --> confirmed: officer confirms
suggested --> changed: officer changes (with a reason)
}
The same flow for a court, a jail and a Union Digital Centre: they differ only in what they are filing about, and in whether the applicant can be sent an SMS.
The lawyer itemises, the court taxes it line by line. A bill is not accepted or refused whole: the court sets what it allows on each line and must say why whenever that is less than was claimed.
stateDiagram-v2
[*] --> draft: the officer closed the case; its lawyer starts a bill
draft --> draft: lines added or taken off, priced against the schedule
draft --> submitted: sent to the court that heard the case
submitted --> verified: court allows or cuts each line (a cut needs a reason)
submitted --> returned: court sends it back for correction (20+ character reason)
submitted --> rejected: court refuses it (20+ character reason)
returned --> draft: the lawyer corrects it
verified --> released: released against a voucher number
released --> [*]
rejected --> [*]
note right of submitted
Only a closed case can be billed,
and only once. Another court's bill
and an unsent draft are both "not found".
end note
Each line is claimed under one head, and each head has its own ceiling, whether it needs a
receipt reference, and whether it can appear more than once on a bill (an appearance per
hearing; one vakalatnama). The ceilings are the district committee’s, in
server/app/services/gazette.py, and a bill records the schedule version it was checked
against, so an old bill still reads against the ceilings that applied to it.
sequenceDiagram
autonumber
actor Staff as Court, jail or centre
participant Dash as court-, prison- or udc-dashboard
participant Srv as Backend
participant NID as NID registry
participant DL as DLAO dashboard
participant Law as Lawyer dashboard
Staff->>Dash: opens a party, prisoner, or the person at the counter
Dash->>Srv: POST …/ekyc (NID, date of birth, name)
Srv->>NID: look the NID up
NID-->>Srv: record
Srv-->>Dash: verified, registry's details fill the form
Staff->>Dash: applicant signs on screen or scan is uploaded
Dash->>Srv: POST …/applications (client_ref, e-KYC id, signature)
Srv->>Srv: triage, duplicate check, audit, in custody = at least high priority
Srv->>Srv: link the court case, or the prisoner and all their cases
Srv-->>Dash: tracking number (by SMS too, if the applicant has a phone)
Staff->>Dash: POST …/applications/ref/documents for each paper they brought
Srv->>Srv: T6 reads each file and rebuilds what the case still needs
Srv-->>DL: appears in the queue, marked with the court, jail or centre
DL->>Srv: assigns a panel lawyer
Srv-->>Dash: stage, lawyer and next hearing follow the case
Srv-->>Law: case appears with its court record and its papers
flowchart LR
subgraph Records["Records"]
CC["Court case<br/>parties, hearings, lawyers,<br/>cause list, restricted flag"]
PR["Prisoner<br/>custody, ward, cases held on"]
APP["Application<br/>how it came, e-KYC, e-signature"]
end
Court["Court staff"] -->|"own court only"| CC
Jail["Jail staff"] -->|"own jail only; of a court case only<br/>court, number, type, sections,<br/>status, next date, cause list"| PR
Officer["DLAO officer"] -->|"everything linked to a case,<br/>plus previous records by NID or name"| CC
Officer --> PR
Officer --> APP
Lawyer["Panel lawyer"] -->|"own cases only, no NID digits"| CC
Lawyer --> PR
CC -.->|"restricted: never a previous record,<br/>cannot be linked by an officer"| X(["hidden"])
Audit[("Every read, search and write<br/>is in the audit ledger")]
Officer --> Audit
Lawyer --> Audit
The main tables and how they connect (server/app/models/). Only the columns that explain
the relationships are shown.
erDiagram
cases ||--o{ case_parties : has
parties ||--o{ case_parties : "plays a role in"
parties ||--o{ duplicate_reviews : "compared in"
incidents ||--o{ cases : groups
cases ||--o{ referrals : "moves between offices"
cases ||--o{ documents : holds
documents ||--o{ checklist_items : "missing-documents list"
documents ||--o{ signatures : "signed (Ed25519)"
parties ||--o{ signatures : signs
cases ||--o{ lawyer_updates : "reports from court"
cases ||--o{ mediation_sessions : schedules
mediation_sessions ||--o{ mediation_attendance : records
mediation_sessions ||--o{ mediation_notices : "SMS to each party"
mediation_sessions ||--o{ udc_notices : "asks a UDC to reach"
cases ||--o| institution_applications : "submitted by court or jail"
ekyc_checks ||--o| institution_applications : "identifies the applicant"
cases ||--o{ case_record_links : "linked to"
court_cases ||--o{ case_record_links : "linked to"
prisoners ||--o{ case_record_links : "linked to"
court_cases ||--o{ court_case_parties : has
court_cases ||--o{ court_proceedings : "what happened at each hearing"
court_cases ||--o{ court_case_lawyers : "lawyers now and before"
court_cases ||--o{ cause_list_entries : "daily cause list"
prisoners ||--o{ prisoner_cases : "held on"
cases {
string ref "APP-year-n, then DLAS-year-n"
string status "application, active, referred, in_mediation, closed"
string priority "critical, high, medium, low"
string track "advice, mediation, sensitive"
string channel "hotline, udc, online, court, prison, ..."
}
parties {
string nid_hash "keyed HMAC, never the NID"
string nid_last4
string safety_level "standard, restricted, caution, no_contact"
}
audit_entries {
string hash "SHA-256 chain, append-only"
}
sync_receipts {
string idempotency_key "T9 offline batches"
}
audit_entries (a hash chain the ORM refuses to update or delete) and sync_receipts
(idempotent offline batches) stand alone: they refer to cases by reference, not by key.
./start.sh
This starts the NID registry, the backend with its SQLite database (server/dlas.db, created
at startup, with the demo court and jail records added), the four dashboards and an ngrok
tunnel that puts the phone lines and the dashboards on the internet, then prints their
addresses, the Twilio webhooks and whether the AI, speech-to-text and the voice are working
(the voice is checked with ElevenLabs at startup). The first run installs the dependencies and creates
server/.env. Each service’s output is shown with its name and kept in .logs/. Ctrl-C
stops everything, and so does any one service stopping.
| Option | What it does |
|---|---|
--no-ngrok |
No tunnel: only this computer can use the dashboards, and everything but real phone calls works |
--no-dashboard |
Backend only (no dashboards) |
--reset-db |
Starts with an empty database (after a schema change); the old file is kept as a backup |
--no-seed |
Leaves out the demo court cases, cause lists and prisoners |
--install |
Reinstalls every dependency first |
The tunnel uses PUBLIC_BASE_URL from server/.env as its domain, so the Twilio numbers
keep working between runs. Without one, ngrok picks a new URL each time; the backend is
given that URL, and the script warns you to repoint the numbers. The script needs uv (or
Python 3.12), Node.js and a signed-in ngrok.
Through the tunnel, anyone with the address can open the dashboards, on the backend’s own public URL:
| Dashboard | From anywhere | On this computer |
|---|---|---|
| DLAO | <public URL>/dlao-dashboard/ |
http://localhost:5173 |
| Panel lawyers | <public URL>/lawyer-dashboard/ |
http://localhost:5174 |
| Courts | <public URL>/court-dashboard/ |
http://localhost:5175 |
| Jails | <public URL>/prison-dashboard/ |
http://localhost:5176 |
| Union Digital Centres | <public URL>/udc-dashboard/ |
http://localhost:5177 |
The public URL (for example https://reach-parched-pastor.ngrok-free.dev) is the one
start.sh prints. ngrok sends each dashboard’s path to that dashboard and everything else to
the backend. The dashboards there are production builds (served from ports 4173–4177) that
call the backend at the public URL. They are rebuilt whenever their code changes: reload the
page to see a change. On this computer the dev servers keep hot reload and call the backend at
http://localhost:8000. A dev server’s page is around 185 requests, more than ngrok’s free
plan lets through at once.
API_TOKEN, the dashboards carry it to every visitor, and start.sh warns
you.Or start each part by hand:
# 1. NID registry
cd nid-server && uv venv --python 3.12 .venv && uv pip install -r requirements-dev.txt --python .venv/bin/python
.venv/bin/uvicorn app.main:app --port 8100 &
# 2. Backend (SMS stays a dry run unless you configure ADN and SMS_DRY_RUN=false)
cd ../server && uv venv --python 3.12 .venv && uv pip install -r requirements-dev.txt --python .venv/bin/python
cp .env.example .env # NID_SERVER_URL=http://localhost:8100 is already set
.venv/bin/python -m scripts.seed_records # demo court cases, cause lists and prisoners
.venv/bin/python -m scripts.seed_cases # and the DLAO dashboard's demo cases
.venv/bin/uvicorn app.main:app --port 8000 &
# 3. Dashboard, showing the backend's cases
cd ../dlao-dashboard && npm install
echo "VITE_API_URL=http://localhost:8000" > .env.local
npm run dev &
# 4. Panel lawyers' dashboard, on the same backend (port 5174)
cd ../lawyer-dashboard && npm install
echo "VITE_API_URL=http://localhost:8000" > .env.local
npm run dev &
# 5. Courts', jails' and centres' dashboards, on the same backend (ports 5175-5177)
for app in court-dashboard prison-dashboard udc-dashboard; do
(cd ../$app && npm install && echo "VITE_API_URL=http://localhost:8000" > .env.local && npm run dev &)
done
# 6. The citizen's app, as a development build on an Android emulator or phone
# (the emulator reaches this computer's backend at 10.0.2.2:8000)
cd ../user-mobile-app && npm install && npx expo run:android
To follow an application from a jail: on the jail dashboard, sign in as Nasima Khatun
(JS-08), open Jalal Uddin (RCJ-2026-0412) and apply for legal aid. Verify him by e-KYC
(NID 2854106397, born 5 June 1990), let him sign on screen and submit. The case is in the DLAO
dashboard’s queue at high priority, marked In custody; its Court and jail records tab shows
his charge, the lawyer who withdrew, his next listing and his previous case. Assign it to a
panel lawyer, and the jail sees the lawyer’s name at once. Court staff do the same from the
court dashboard (CS-11), for anyone on their register.
To follow an application from a Union Digital Centre: on the centre’s dashboard, sign in as
Rehana Parvin (UDC-MTP) and file an application. Verify the applicant by e-KYC (Rahima Begum,
NID 6390284417, born 11 February 1992), give a mobile number, tick that she cannot read, add
a paper or two, let her sign on screen and file it. She is sent the tracking number by SMS, and
the DLAO dashboard’s queue has the case with the papers on it and cannot read or write against
her name. Leave the number out instead, and the centre is told to write the tracking number down
because that slip is all she will have.
To follow a case from the officer to the lawyer and back: assign a panel lawyer on the DLAO
dashboard (for example Adv. Nasrin Jahan, LAW-12), sign in as that lawyer on the lawyers’
dashboard, send an update from court, then open the case’s Court progress tab on the DLAO
dashboard.
Try a call without a phone line at http://localhost:8000/docs. Start with
POST /intake/conversations, then send each answer to /intake/conversations/{id}/turns.
nid-server/README.md lists people you can call as, such as Rafiqul
Islam applying for his mother.
To hear the voice path (speech-to-text, the agents and the spoken replies), set
OPENAI_API_KEY and the ElevenLabs keys in server/.env (and LLM_PROVIDER=openai for
the agents to use gpt-6-luna), then call a line from recorded answers with
scripts/simulate_call.py. For real calls through Twilio, see Telephony in
server/README.md.
The registry’s people are fictional, but their phone numbers may belong to real subscribers. Keep
SMS_DRY_RUN=true, or list only your own numbers inSMS_ALLOWLIST, while it is connected.
deploy/deploy.sh runs everything on the VPS. The backend and the NID registry run under
pm2 on localhost ports. The dashboards and the legal aid app are static builds that nginx
serves, each on its own appbaksho.com subdomain over HTTPS. For example, the DLAO
dashboard is at https://dlao.appbaksho.com and the backend at
https://dlas-api.appbaksho.com. deploy/README.md covers the hosts
and ports, the DNS records, redeploying, the settings and the Twilio webhooks.
Run each module’s checks from its own folder. Every command below is also listed in that module’s README.
| Module | Tests | Lint and format | Types | Build |
|---|---|---|---|---|
server/ |
.venv/bin/pytest (unit, API and a simulated phone call) |
.venv/bin/ruff check . && .venv/bin/ruff format --check . |
.venv/bin/mypy |
docker build -t dlas-backend . |
nid-server/ |
.venv/bin/pytest (API, matching, registry integrity) |
same as the server | .venv/bin/mypy |
docker build -t nid-registry . |
| each dashboard | npm test (Vitest, jsdom) |
npm run lint (ESLint, including React Compiler rules) |
npm run typecheck |
npm run build (type-check, then production build) |
user-mobile-app/ |
npm test (Jest, jest-expo) and cd android && ./gradlew :sos-gesture:testDebugUnitTest (JUnit, after a prebuild) |
npm run lint |
npm run typecheck |
npx expo run:android (development build) |
If your shell exports a PYTHONPATH (ROS, for example), run the Python tools with
env -u PYTHONPATH … so foreign pytest plugins are not loaded.
What the backend’s tests cover, by area: agents (test_agents, test_hotline_menu,
test_helpline, test_llm), telephony and voice (test_audio, test_speech_to_text,
test_simulate_call), the API for each role (test_api_intake_dlao, test_api_lawyer,
test_api_court, test_api_prison, test_api_documents_mediation, test_api_applications,
test_api_t2_t3_t4), identity and records (test_nid_registry, test_ekyc,
test_records_access, test_seed_records), and safety (test_safe_contact, test_notices,
test_mediation_notices, test_crypto, test_models).
To refresh the dashboard’s API contract fixture after changing the backend’s response
shapes: cd server && .venv/bin/python -m scripts.dashboard_fixture.
Copy server/.env.example to server/.env (start.sh does it on the first run). Every key is
optional in development. The ones that matter most:
| Setting | What it does | Default |
|---|---|---|
ENVIRONMENT |
production turns on Twilio signature checks and refuses unsafe defaults |
development |
DATABASE_URL |
SQLite in development, Postgres in production | server/dlas.db |
API_TOKEN |
Shared bearer token the dashboards send | empty (open) |
CORS_ORIGINS |
Dashboard origins allowed to call the API (ports 5173 to 5177 by default) | localhost |
NID_HASH_KEY |
Key for the HMAC of stored NIDs; production refuses the development value | change-me |
NID_SERVER_URL, NID_SERVER_API_KEY |
Where the registry is, and its key | http://localhost:8100 |
LLM_PROVIDER, ANTHROPIC_API_KEY, OPENAI_API_KEY |
Which optional model the agents consult | anthropic, no key |
SMS_DRY_RUN, SMS_ALLOWLIST, ADNSMS_* |
ADN SMS. Keep SMS_DRY_RUN=true, or list only your own numbers, while the registry is connected |
dry run |
HELPLINE_NUMBER |
The number printed in every SMS | 16430 |
PUBLIC_BASE_URL, TWILIO_AUTH_TOKEN |
Public address for Twilio’s webhooks, and the signature check | empty |
ELEVENLABS_*, VOICE_SPEED, VOICE_START_BUFFER_S |
The line’s voice | see .env.example |
STT_END_OF_TURN_MS, STT_STORY_END_OF_TURN_MS, STT_MIN_SPEECH_RMS, OPENAI_STT_* |
Speech-to-text turn detection and accuracy | 700, 1200, 500 |
MEDIATION_NO_SHOW_LIMIT, EKYC_CHECK_VALID_MINUTES |
Two missed sessions flag a UDC notice; an e-KYC check lasts 120 minutes | 2, 120 |
Each dashboard reads VITE_API_URL (and optionally VITE_API_TOKEN) from its
.env.local. nid-server reads NID_API_KEY, NAME_MATCH_THRESHOLD (85), DATA_FILE and
ENVIRONMENT from its own .env.
For production set at least ENVIRONMENT=production, DATABASE_URL (Postgres), API_TOKEN,
NID_HASH_KEY, TWILIO_AUTH_TOKEN, SMS_DRY_RUN=false with the ADN credentials,
NID_SERVER_URL and NID_SERVER_API_KEY, HELPLINE_NUMBER, OPENAI_API_KEY, the UDC
entrepreneurs’ real numbers in server/app/services/udc.py, and LLM_PROVIDER with its key
if the agents should use a model. See the Known limitations in
server/README.md before any real deployment (no
per-user sign-in yet, rosters are code, no migrations, single-worker conversation state).
vanguard/
README.md this file
start.sh starts everything (NID registry, backend, ngrok, four dashboards)
deploy/ deploy.sh, the VPS's pm2 apps and nginx sites
server/ FastAPI backend
app/ main.py, config.py, database.py
models/ agents/ routers/ services/
scripts/ seed_records, seed_cases, simulate_call, dashboard_fixture
tests/
nid-server/ National ID registry
app/ main.py, registry.py, schemas.py, data/citizens.json
tests/
dlao-dashboard/ officer dashboard (src/pages, api, state, i18n, lib, components)
lawyer-dashboard/ panel lawyers' dashboard
court-dashboard/ courts' dashboard
prison-dashboard/ jails' dashboard
udc-dashboard/ Union Digital Centres' dashboard
portal/ the front page: hotline number and a link to each app (index.html)
user-mobile-app/ the citizen's Android app (src/app screens, modules/sos-gesture)
vanguard-digital-leagal-aid/ a local reference PWA, kept out of git (see .gitignore)
| Symptom | Likely cause and fix |
|---|---|
| The backend fails with a database error after pulling | The schema changed and there are no migrations yet. Run ./start.sh --reset-db (the old file is kept as a backup) or delete server/dlas.db |
| A dashboard shows sample data, not live cases | VITE_API_URL is not set in its .env.local. Set it to http://localhost:8000 and restart npm run dev |
| A dashboard’s requests are blocked by the browser | The backend’s CORS_ORIGINS does not include the dashboard’s port |
| Sign-in on the court, jail or lawyer dashboard fails with a live backend | The ID is not on the roster (server/app/services/courts.py, prisons.py, panel.py) |
| A court or jail application says the identity could not be checked | The NID registry is not running or NID_SERVER_URL is not set. Staff can send it without e-KYC and verify later |
| Callers are not asked the NID security questions | Same cause: without the registry, intake skips them and records the applicant as unverified |
| A phone call is answered with a spoken “cannot take applications by phone” | The voice is broken. Check GET /health for voice, then ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID and ELEVENLABS_MODEL_ID (it must speak Bangla) |
| A caller hears they cannot be heard, and the call ends | Speech-to-text is off or rejected: set OPENAI_API_KEY and check the log for the reason |
| The line is interrupted by noise, or misses quiet callers | Raise or lower STT_MIN_SPEECH_RMS |
| Twilio cannot reach the backend | No tunnel. Run ./start.sh without --no-ngrok, and set PUBLIC_BASE_URL so the domain does not change between runs |
| SMS are not arriving | SMS_DRY_RUN is true (the default), the number is not in SMS_ALLOWLIST, or the notice is held for an officer because the case is sensitive or do-not-call |
| pytest loads unfamiliar plugins | Your shell exports PYTHONPATH. Use env -u PYTHONPATH .venv/bin/pytest |