Inspiration

A small NDIS provider signs around forty documents a week — service records, invoices, incident reports, policy forms. Every one has to match rules that run to hundreds of pages and change without anyone telling you. Miss one and it isn't a typo, it's an audit finding.

The obvious fix is "ask an LLM." We tried that first and it failed in the worst possible way: confidently, fluently wrong. It invented regulation numbers. For a compliance tool that isn't a rough edge, it's a disqualifying defect — a fabricated citation is more dangerous than no answer at all, because the customer files it.

So we set one hard constraint before writing any code: every verdict must cite a rule that actually exists, from a versioned ruleset we control. Everything else in ComplianceGuardian follows from that decision.

What it does

Upload a document — PDF, text, CSV, or image.

Document lifecycle

  1. Ingestion Agent extracts the facts with Gemini.
  2. Compliance Agent reasons over them against the real ruleset for your industry and your jurisdiction, returning a risk score where every verdict cites the rule it came from.
  3. Routine documents auto-approve. Genuine edge cases escalate to a human reviewer with the reasoning attached.
  4. Every decision is appended to an immutable audit trail.
  5. A scheduled workflow produces weekly reports with a Gemini executive summary.

Today that's 16 rulesets, 88 individual rules, across 12 jurisdiction profiles — NDIS and aged care in Australia, corporate compliance in India, and data privacy under GDPR, CCPA, PIPEDA, LGPD, PDPA and POPIA.

How we built it

Five containerised services on Cloud Run behind one API gateway, all declared in Terraform, running in a real GCP project — no emulators, no mocks.

System architecture

Two design decisions we'd defend in any review.

Tenant isolation is structural. Tenant identity comes from verified JWT claims only, never from a request body or query parameter. There is no code path that can read across tenants, because there is nothing for an attacker to tamper with.

The audit trail is immutable at the IAM layer, not the application layer. Rewriting a BigQuery row needs two permissions: bigquery.tables.updateData to change the data, and bigquery.jobs.create to start the job that does it. The identity that writes our audit trail holds the first and is deliberately denied the second. UPDATE and DELETE are not merely forbidden by our code — they are unavailable to that identity. Our own application cannot rewrite history.

Why the audit trail cannot be rewritten

Entitlements use counters rather than a boolean, spent inside a Firestore transaction, so \( \text{remaining} = \text{granted} - \text{consumed} \) holds even under concurrency. A naive "have they used their free report?" flag loses that race and gives away two reports for one payment.

Challenges we ran into

Making the model provably honest. Prompting alone doesn't stop fabrication. Rules live in versioned YAML, citation is mandatory in the output contract, and prompt version and model version are recorded against every check so any verdict can be reproduced later. When Gemini can't ground a verdict, it escalates instead of guessing.

"Deployed" is not "working." Our weekly report workflow existed, was scheduled, and looked healthy in the console — and had never executed once in three weeks. Cloud Scheduler was being rejected with PERMISSION_DENIED every Monday into a log nobody was reading. Fixing it revealed six defects stacked behind one another: a missing invoker role, a doubled URL scheme, a request aimed at a service that didn't serve that route, a hardcoded tenant list, a service account with no Firestore access, and a missing log-writer permission. Each fix only exposed the next.

We had no monitoring, and that was the actual bug. Every failure above was individually minor. What made them serious was that nothing was watching. There are now uptime checks and alerts on any scheduler or workflow failure — the single alert that would have caught this on day one.

Infrastructure defaults that look safe and aren't. A Terraform apply silently dropped an environment variable that had been set by hand, and every platform admin lost console access at once — Cloud Run replaces its env list wholesale rather than merging it. We later found the same trap in our payment flags: a default of false looked cautious, but on a fresh checkout it would have quietly stripped the PayPal credentials off the gateway and ended international payments without failing anything. Those variables are now required, so a missing value stops the deploy instead.

Taking money from outside India. An unregistered domestic Razorpay account rejects international cards even in live mode, silently excluding most of our addressable market. Stripe's INR conversion fee turned a $50 audit into ₹4,948 at checkout versus ₹4,200, so we removed it entirely and use PayPal for international customers.

Accomplishments that we're proud of

  • It's genuinely live — real GCP project, real Gemini, real Firebase Auth, real payments that can take real money from twelve jurisdictions.
  • 536 passing tests, including the concurrency case where two simultaneous requests try to spend the same last report allowance.
  • An audit trail our own code cannot alter, enforced by IAM rather than by intent.
  • Graceful degradation. When our Gemini credits ran dry mid-build, documents failed cleanly and reports fell back to a clearly labelled fixture. The product never claimed to have done work it hadn't.
  • 88 cited rules across 12 jurisdiction profiles, so a non-Australian customer isn't quietly assessed against Australian rules — an early bug we caught and fixed.

What we learned

Fluency is not accuracy, and in compliance the gap between them is the whole product. The engineering that mattered wasn't the prompting — it was building a system where a confident wrong answer is structurally hard to produce and impossible to hide.

We also learned that the dangerous failures are the quiet ones. Nothing crashed. The weekly report didn't error, it simply never ran. Report covers printed internal IDs instead of business names and no exception was raised. A system that fails loudly is a system you can fix; a system that fails silently just accumulates.

What's next for ComplianceGuardian

  • True recurring subscriptions. Pro is currently granted per payment rather than auto-renewing — honest, but it should renew.
  • Ruleset change detection, so when a regulator amends a rule we re-check the documents that relied on it and tell the customer which ones moved.
  • More jurisdictions, prioritised by where customers actually sign up.
  • Evidence export — one auditor-ready bundle of decisions, citations and the immutable trail behind them.
  • Narrowing the reviewer loop, so escalations arrive with a suggested remediation rather than only a flag.

Built With

Share this project:

Updates