An on-premise AI assistant for Swiss professional practices, built so that a compromised language model still cannot make the server do anything.
Single-tenant AI workspace for Ticino law firms, private banks, fiduciaries, wealth managers and notaries — where client data sits under professional secrecy (Art. 321 StGB), FINMA supervision and the Swiss FADP, and cloud AI is not an option.
Sole architect & engineer
Product, backend, frontend, security, infrastructure, compliance docs
~7 weeks
29 Jun → 15 Aug 2026, active development
Ticino practices
Law, private banking, fiduciary, wealth management, notarial
Both quality gates were re-run while writing this case study: pytest → 878 passed · vite build → 1755 modules, 234 KB gzip. No test touches the network.
passing backend tests
API endpoints
reviewed data connectors
declarative AI tasks across 8 verticals
lines of backend code
lines of frontend code
lines of test code
database tables · 11 migrations
Swiss professional practices sit on exactly the kind of data that makes cloud AI a non-starter: client files under professional secrecy, banking data under FINMA supervision, personal data under the revised Swiss FADP and GDPR. A law firm in Lugano cannot paste a client mandate into ChatGPT — not because the output would be bad, but because doing so is a disclosure.
The product sells security and discretion first, features second. That inverts the usual priority order and it shows up everywhere in the codebase: a security or data-honesty defect outranks a missing feature. Three constraints shaped every decision:
Each firm gets its own instance. No shared database, no cross-customer query paths to get wrong.
Uploaded document text lives in a process-local dictionary and never touches disk. Chat content is persisted only when an operator explicitly enables TTL-expiring content logging.
Not with a destination, not with a permission, not with a fact. The untrusted component is structurally denied authority — by code, not by policy.
The dashboards feature exists because a wealth manager wants a market desk. The design exists because that feature hands a language model a request-generating capability inside a private network. Four boundaries refuse that — each enforced by code:
The model's only machine-readable output is a dashboard specification validated fail-closed. No field can hold a URL, hostname, port, path, header, credential, file path or code — and there is no arithmetic, no template string, no formula. A perfect prompt injection yields a wrong-but-inert dashboard, never a request.
A spec names a connector by registry id and passes typed parameter values. Host, path, method, headers and credentials are constants in reviewed Python. When the model wants a source that doesn't exist, it can only file a request into an admin queue — only a person adds a data source.
Load spec → validate → fetch → normalise → render. Fetched payloads never flow back into a prompt, so a hostile upstream response cannot become prompt injection. The docstring explicitly warns the next engineer not to add a "summarise this tile" feature without re-reading it.
Ownership and sharing are enforced in the query on every path — list, search, get, patch, delete, and the context fed into a model turn. A user who completely jailbreaks the assistant still cannot read a colleague's work, because the assistant is not what decides.
"The model is never trusted with authority. Not with a destination, not with a permission, not with a fact."
Each control's actual strength is written down in the codebase — including the ones that are only tripwires, and the one residual risk that stays open by documented decision.
Regex prompt-injection detection whose own docstring reads: "this is for alerting, not prevention." It audits the realistic office-walk-in case; the structural boundary is what protects the server.
An allowlist says nothing about what an approved host sends back. Payload structure is bounded (depth, keys, lengths, 256 KB cap), declared types are enforced, and prose fields are escaped while numeric fields reject markup outright.
Sharing outside the firm freezes fetched figures behind an unguessable token. Opening one is a SELECT — no fetch, no connector, no model — so an anonymous visitor can never cause outbound calls from inside a firm's network.
Where the URL genuinely is user-supplied (prospect enrichment), every resolved address must be globally routable, redirects are walked by hand with each hop re-checked, and 46 dedicated tests assert a refused target produced no request at all — not merely no result.
A dashboard tile is ok, stale (greyed, labelled with its age) or error. Never a zero, never a blank, never interpolated, never mock — enforced through snapshots and PDF exports too, because a document outlives the screen it came from.
Hallucination here is a liability event, so one contract — a procedure, not an exhortation — is written once and imported into every prompt: ask when unsure, never invent figures or citations, mark what is source, knowledge or assumption. "I don't know" is a correct answer.
The workspace as a firm sees it — captured from the live demo, which runs in Italian for its Ticino audience. Note the honesty label on every market figure: indicative, possibly delayed, stamped with the time it was fetched.
A market desk dashboard — SMI with day and 52-week ranges, UBS and Nestlé quote tiles, curated market news. The header reads "Version 3": every edit went through propose → review → save, and one click exports the desk to PDF or Word.
Chat in the private-banking vertical, rendering portfolio blocks — composition donut and performance chart drawn by the hand-built SVG renderer, with history fetched and metrics computed server-side, never by the model. Task chips sit above; "messages are not saved" is stated below.
The document library — a change-of-conditions template with declared {{placeholders}}, server-side autofill ("empty fields are flagged, never invented") and the A4 paper-sheet preview that breaks where the printed copy breaks.
A complete workspace — 8 professional verticals, 42 declarative AI tasks, live market dashboards, a document library and a built-in go-to-market funnel.
Editing a sent message forks a sibling branch instead of overwriting history — both responses survive and the user switches between them. Every read path filters on the owner in SQL.
Assistant replies carry strict JSON blocks — tables, charts, stats, forms, tickers — rendered by hand-built SVG in the browser, by docx/pptx on the server, and by PDF. Charts in documents are followed by their figures as real text.
"The SMI and USD/CHF every morning" becomes a validated spec, with a repair loop and propose → review → save on every edit. Masonry grid, drag-and-drop, append-only version history, weekday-morning snapshots.
Swiss National Bank, ECB, FINMA news, Yahoo Finance, Swiss and Italian financial press, GDELT — plus computed indicators and portfolio-weighted figures, calculated server-side, never by the model.
Chat attachments live in RAM only and die on restart; the library persists what a user explicitly filed. AI drafting proposes, the user saves — nothing is persisted that nobody decided to keep. Deterministic nLPD/GDPR quality checks always run.
One pipeline renders chat replies and dashboards to docx, pptx and PDF — with an A4 paper-sheet preview paginated by real measurement, so the preview breaks where the printed copy breaks.
Self-service demo requests, SSRF-guarded website enrichment, ideal-customer scoring, human approval, sector-seeded public demos behind guest identities, and outreach email with a dry-run default and the Swiss UWG opt-out appended automatically.
Append-only JSONL audit trail, opt-in TTL content logging, retention policy as data, dependency-ordered purges and first-class user offboarding — plus a cron watchdog that emails when the service degrades.
A flat ink-on-paper design system with measured AA contrast, ~650 bilingual UI strings, hand-built SVG charts with no charting library, and an 11-step guided tour that drives the browser to each feature's own page.
Drawn in the product's own design language: zero border radius, 2 px borders, no shadows. The single-worker assumption is explicit — the codebase states in four places that if it ever changes, every process-local cache moves to Redis together.
TLS · HSTS · CSP · rate limiting · serves the built frontend · reverse-proxies /api — single origin, deliberately no CORS anywhere
Alembic migrations at startup · JWT in httpOnly SameSite cookies backed by revocable session rows · RBAC as one reviewed map (28 actions × 4 roles) · provider-abstracted LLM layer (OpenAI · mock · on-prem)
12 tables · metadata only by default · append-only dashboard version history
External cron, every job runnable by hand — debuggable at 09:00 when a desk reports a missing snapshot.
Testing
878 tests on in-memory SQLite with a mocked LLM — no network. Coverage weighted toward the boundaries: 46 tests on the SSRF guard alone.
Configuration
47 typed settings with safe defaults: email dry-run on, content logging off, and a refusal to boot in production with the shipped JWT secret.
Documentation as a deliverable
Deployment guide, runbook, incident response, DPIA against Swiss FADP/GDPR, business continuity plan with RPO/RTO — each also produced in Italian.
Named on purpose — a portfolio that only lists wins is not a portfolio, and the codebase names them itself.
Not "we sanitise inputs" — a designed system where the untrusted component is structurally denied authority, where each control's actual strength is written down, and where an accepted residual risk is documented next to the code that carries it.
The same outbound-HTTP problem is solved two different ways in the same codebase, because in one case the destination is a constant and in the other it is a stranger's input. Recognising that those need different controls is the skill.
Sector-gated features because an irrelevant dashboard is a worse first impression than none. A tour that navigates because explaining a page from a different page does not work. A funnel because a demo nobody can reach sells nothing.
Data model through migrations, API, streaming, hand-built visualisation, design system, bilingual copy, Docker, nginx, cron, backups, incident response, DPIA and business continuity — for a product with a real deployment and a real funnel behind it.
Get a custom AI platform, an on-premise deployment, or a full SaaS product engineered with the same discipline — from threat model to backup scripts.