Sauti (Swahili: voice) + Ledger — a public, verifiable record of community voice.
SautiLedger is a Kenya-first PeaceTech platform that turns informal, scattered community concerns into anonymized, structured, and verifiable Community Mandates that institutions can acknowledge, track, and resolve in public.
It is designed for the gap between two things that already exist:
- Communities have a lot to say. Concerns about water, health, education, security, infrastructure, and corruption are constantly raised — in WhatsApp groups, on the radio, in barazas, in market conversations, and to chiefs and ward administrators. Most of it never reaches the institutions that could act on it, and what does reach them rarely lands as evidence.
- Institutions struggle to respond systematically. County governments, constituency offices, ward administrations, and national agencies have legitimate accountability obligations under Kenya's public participation framework (Constitution Art. 10 & 174, County Governments Act, Public Participation guidelines) but lack a structured, low-friction channel to receive citizen input and demonstrate response over time.
SautiLedger sits in that gap.
- A citizen signs in or registers with a phone number and password, then submits a concern — in English, Swahili, Sheng, or mixed language — from a mobile-first, low-bandwidth interface. Phone numbers are the most accessible identifier for the communities we serve, so they're the only credential required. They pick the responsible scope (national, county, constituency, or ward) and the relevant Kenyan boundary via cascading dropdowns.
- The phone number is never exposed. It's normalized to Kenyan E.164 and stored only as a salted SHA-256 hash internally. The backend stores the original text safely and issues an anonymous tracking code the citizen can use to follow the issue later — nothing on any public surface ever ties a submission back to the citizen.
- AI normalizes the message — detects language, translates as needed, classifies the issue (e.g. water, health, education, infrastructure, security, governance), scores urgency, derives the responsible office from scope + location, and drafts a formal mandate. All AI output is marked as generated, is reviewable, and is editable.
- Similar submissions are clustered into a single Community Mandate — an anonymized, plain-language statement of a shared community priority backed by a count of supporting submissions and an evidence-strength signal.
- A public dashboard surfaces the mandates by scope, county, constituency, ward, category, urgency, and status — graph-rich and easy to scan. Individual personal data is never exposed; only aggregates.
- Registered citizens can upvote mandates they care about, strengthening the public signal without exposing identity.
- Institutions respond in public. Authorized institution users acknowledge mandates, post updates, dispute claims with reasoning, and mark issues resolved. Every state change is recorded.
- A Responsiveness Index measures response behavior — acknowledgement speed, update frequency, resolution rate — so accountability is about how institutions act, not which areas have the most problems.
- For citizens: a safe, anonymous, low-bandwidth way to be heard without retaliation risk, and a way to track whether their concern was acknowledged and acted on.
- For institutions: a structured, auditable inbox of community priorities with clear scope routing, plus a public record of their own responsiveness they can point to during budget cycles, performance reviews, and reporting under the County Governments Act.
- For civil society, journalists, and researchers: a transparent, machine-readable feed of aggregated civic priorities and institutional response patterns over time — without exposing the individuals behind the submissions.
- For peacebuilding: by routing grievances into a calm, nonpartisan, institution-focused channel and showing measurable response, SautiLedger reduces the friction that turns unaddressed concerns into mistrust, rumor, and unrest.
- Privacy by default. Public views show aggregated mandates; raw submissions, phone numbers, and exact GPS never appear publicly. Phone numbers are stored only as a salted SHA-256 hash internally so we can still link a citizen to their own tracking codes without exposing identity.
- Nonpartisan, institution-focused. Language targets responsible offices, not individuals or political actors.
- Reviewable AI. Every AI-generated category, urgency, routing, and mandate draft is editable, marked as generated, and overridable.
- Mobile-first, low-bandwidth. Small payloads, resilient flows, clear copy in everyday language.
- Kenya-first, but configurable. Counties, constituencies, and wards are data, not hard-coded UI.
The full product brief lives at docs/sautiledger_project_brief.md.
AI coding agents should start with:
AGENTS.mddocs/ai/product-context.mddocs/ai/implementation-plan.mddocs/ai/domain-model.mddocs/ai/ai-processing-contract.mddocs/ai/agent-roles.md
The MVP is Kenya-focused and uses:
- Vite, React, TypeScript, shadcn/ui, lucide-react, and Recharts for the frontend.
- Express.js, TypeScript, and TypeORM for the backend.
- SQLite (file-based) for persistence — zero ops, perfect for the demo. The DB lives at
apps/api/data/sautiledger.dband the schema is auto-created via TypeORMsynchronize: true. - Docker Compose for a single-container local run.
- Kubernetes for deployment.
- OpenAI APIs for language detection, translation, classification, summarization, and mandate generation.
There is no separate "Authority" table in the MVP. When citizens submit a concern, they pick the responsible scope themselves:
- National — Office of the President
- County — the relevant county government
- Constituency — the relevant constituency office
- Ward — the relevant ward administration
The responsible office string is derived from the chosen scope plus the citizen's location.
The React frontend is served by the Express app in production-like runs.
Copy .env.example when app scaffolding exists and fill in local values. Do not commit real secrets.
apps/web: Vite + React + TypeScript frontend.apps/api: Express + TypeScript REST API with TypeORM wiring (SQLite).packages/shared: shared domain types used by the API and frontend.docker-compose.yml: single-container production-like app stack.
npm installCopy .env.example to .env and fill in local values:
cp .env.example .envKey variables:
| Variable | Purpose |
|---|---|
DATABASE_PATH |
Path to the SQLite file (default data/sautiledger.db). |
AI_PROVIDER |
mock for offline demos, openai for real LLM calls. |
OPENAI_API_KEY / OPENAI_MODEL |
Required only when AI_PROVIDER=openai. |
SUBMISSION_HASH_SALT |
Server-side salt for SHA-256 hashing of phone numbers. |
SESSION_SECRET |
JWT signing secret for citizen sessions. |
INSTITUTION_DEMO_KEY |
Shared key required by the institution console (X-Institution-Key). |
CORS_ORIGIN |
Allowed origin for the Vite dev server. |
Do not commit real secrets.
npm run seed:demo --workspace @sautiledger/apiPopulates the SQLite DB with a small set of clustered demo mandates and submissions so the dashboard and mandates list have something to render on first run. The phones and password used by the seed script live in apps/api/src/scripts/seed-demo.ts — change them before sharing the instance publicly. The seed is idempotent — safe to re-run. The SQLite file and parent directory are created automatically on first run.
In two terminals:
npm run dev:api
npm run dev:web- Frontend: http://localhost:5173
- API: http://localhost:3000/api
The Vite dev server proxies /api requests to the API.
A citizen account (phone + password) is required to submit. Register at /auth/register — phone numbers are the most accessible identifier for our users, and they're stored only as a salted hash internally; nothing on public surfaces is ever tied back to the citizen.
To use the institution console at /institution, paste the value of
INSTITUTION_DEMO_KEY from your .env (default: demo-institution-key).
docker compose up --buildThe app is served from http://localhost:3000 (Express serves the built React frontend).
- Visit http://localhost:5173 — the public dashboard renders charts powered by any seeded or submitted mandates.
- Register a citizen account from
/auth/register(phone + password) and sign in. Submissions require a logged-in citizen so tracking codes can be linked back to that citizen — but never publicly. - Open Submit a concern and post a new informal report. The API generates a tracking code and either joins an existing Community Mandate or creates a new one (mock AI by default).
- Open My submissions to see your recent tracking codes.
- Browse
/mandatesto filter by scope, county, constituency, ward, category, and urgency. - Visit
/trackingand paste a tracking code to see anonymous status updates. - Open
/institution, enter the institution key from your.env, then acknowledge or update one of the mandates.
AI_PROVIDER=openai
OPENAI_API_KEY=sk-...
OPENAI_MODEL=gpt-5 # or another chat-completions modelBoth processing (processSubmissionWithAi) and clustering (matchSubmissionToMandate) call the OpenAI API with strict JSON Schema responses. The original submission text is always preserved separately from AI-generated summaries, and every AI-produced field is marked generated: true.
npm run test --workspace @sautiledger/api # unit tests (mock AI, no DB)
INTEGRATION=1 npm run test --workspace @sautiledger/api # adds register→submit→track integration tests (uses the SQLite file DB)- Submissions never expose raw names, phone numbers, exact GPS, or identifiers on public surfaces.
- Phone numbers are normalized to Kenyan E.164 and stored only as a salted SHA-256 hash (
SUBMISSION_HASH_SALT). - Public dashboards show aggregated Community Mandates, not individual submissions.
- AI-generated summaries are stored separately from the original submission text and flagged as generated.
This project was built with OpenAI Codex as the primary coding collaborator — used for scaffolding, iterating on the AI processing pipeline, debugging the Docker build, wiring up the Kubernetes manifests, and drafting documentation. Human review and product direction stayed in the loop throughout.