docketrouter
Security

What we actually do

Every control below is running in DocketRouter today, and each one names the file in our own repository that implements it. 14 of them run on every request or every deploy with nothing missing; 8 are built with something specific still missing, and that is said on the control rather than left out of the list. Controls we do not have at all are not on this page. They are in the readiness document, in a gap list with the cheapest honest first step for each.

What we are not
  • We are not SOC 2 audited. We hold no SOC 2 Type II report, no SOC 2 Type I report and no SOC 3 report.
  • We hold no ISO 27001 certificate and no ISO 42001 certificate, and we make no HIPAA claim.
  • We are not certified by anyone, and we are not compliant with a standard we have never been examined against.
  • Nothing on this page has been examined by an outside auditor. What it has instead is a file name per line, and a test that fails our build if any of those files stops existing.

The control-by-control readiness assessment behind this page, all 22 controls, including the ones we fail, and what an auditor would need from us, is docs/SOC2-READINESS.md in our repository. Ask for it at hello@docketrouter.ai and we will send it as it is, gap list included.

The controls (22)

Ordered the way a diligence questionnaire asks: who can get in, what is encrypted, what we keep, what we check, how a change reaches production.

API keys are stored as a hash, never in the clear
live today

A key is shown to you once at creation. What we keep is its SHA-256 digest plus a display prefix (first ten characters, last four); authentication is a lookup by digest, so the plaintext key exists nowhere in the database or the logs.

src/lib/db.tssrc/lib/auth.ts
Security · Confidentiality
Every key is scoped, revocable and rotatable
live today

A key carries a spend cap and reset period, a jurisdiction, a data policy, and a provider allowlist and order. Rotation replaces the digest in place; revoking disables the row and the upstream sub-key with it.

src/lib/account/keys.tssrc/lib/provider-policy.ts/keys
Security · Confidentiality
Third-party apps get a key through PKCE, and never see your credentials
live, with a gap

An app sends you to our authorize endpoint; only S256 is accepted, the verifier is never stored, the code is held as its SHA-256 digest, it is burned on the first exchange attempt whether or not that attempt succeeds, and the callback URL is validated against open-redirect abuse before a code is minted.

What is missing: The pending-code store is an in-memory map in one process: codes do not survive a restart, and the flow would need a database-backed store before we run more than one instance. Both failure modes end in “expired code”, never in an issued key.

src/lib/oauth-keys.ts/api/oauth/authorize
Security
Signed-in surfaces are protected in one place
live today

Clerk holds the session. The proxy protects the account surfaces before a page renders, and a session cookie minted by a different Clerk instance is dropped rather than passed to the verifier, so an old cookie signs you out instead of producing a 500.

src/proxy.tssrc/lib/auth.ts
Security · Availability
Your files are isolated by account id in every query
live today

Case files and their chunks are filtered by owner id in the query itself, not after the fact, and are retrieved only into your own requests. A matter id you do not own is a 404, so ids are not enumerable.

src/lib/db.tssrc/lib/matters.tssrc/app/api/v1/files/route.ts
Security · Confidentiality
Per-account envelope encryption for case files, with a blind index
live, with a gap

A master key held in the host environment, never in the database, derives a per-account AES-256-GCM data key and a per-account HMAC index key by HKDF salted with the account id. The account id is also the additional authenticated data, so a row lifted into another account fails the tag check instead of decrypting; search runs over the blind index so it survives encryption.

What is missing: Built and tested, switched on per host. It is off by default, and the blind index has to be backfilled while rows are still plaintext before encryption is turned on. Turning it on first would make search return nothing. /policy reports this host's live state.

src/lib/security/crypto.ts/policy
Confidentiality · Security
A customer's own provider key is encrypted with no plaintext fallback
live today

If you bring your own upstream key we hold it as an AES-256-GCM envelope under a per-(account, provider) key derived from a separate master key, bound to both by the authenticated data. With the master key unset or malformed, a write is rejected rather than stored readable, and a read fails closed.

src/lib/byok.ts
Confidentiality · Security
The organisation audit log is append-only in the database, not by convention
live today

Every privileged organisation action writes an audit row with the actor, the action, the target and the request id. Database triggers raise on UPDATE and DELETE against that table on both Postgres and SQLite, so no application code path can rewrite history.

src/lib/db.tssrc/lib/orgs.tssrc/app/api/v1/audit/route.ts/api/v1/audit
Security · Processing Integrity
Rate limits per caller, with the key-issuing path the strictest thing we run
live, with a gap

Token buckets per key, per signed-in user and per IP, reported back as X-RateLimit headers with a Retry-After. The OAuth exchange is deliberately the tightest bucket in the API because it is the path that mints credentials.

What is missing: The buckets are per process and in memory, which is correct for one box and would have to move to shared counters before we run more than one.

src/lib/ratelimit.ts
Availability · Security
Prompts and answers are not logged by default
live today

One usage row per request holds metadata only: key id, model, token counts, cost, latency, citation counts, status, request id, upstream generation id and the provider that served it. Prompt and answer text is stored only if you turn log_content on for that key, which is off by default.

src/lib/db.tssrc/app/api/v1/chat/completions/route.ts/policy
Privacy · Confidentiality
Training and retention are denied upstream on every call, and the policy cannot be widened
live today

Every request carries data_collection “deny” and an allowlist of providers whose published policy is no-training. A no_retention key adds zdr true; if no zero-retention endpoint can serve the model the request fails with a 503 rather than falling back to a retaining one. A request may only narrow the key's policy; anything wider is a 400.

src/lib/provider-policy.tssrc/lib/contract-review/policy.ts/policy
Confidentiality · Privacy
A private-pod key cannot leave the box
live, with a gap

A key marked private_pod may call in-house models only. No upstream provider object is built because there is no upstream, and with no local model configured the route answers 503 and says that no text was sent anywhere, rather than quietly routing out.

What is missing: It is set by us when a key is attached to a pod, not self-serve, and it needs a local model endpoint on the host. /policy reports whether this host has one.

src/lib/contract-review/policy.tssrc/lib/provider-policy.ts/policy
Confidentiality · Privacy
Client identities can be replaced before any model call, with a release gate
live, with a gap

The scrambler rewrites names, organisations and identifiers in a document to placeholders using a local model and a regex pass; the mapping stays on our machine and the answer is restored from it. A gate refuses to send the document at all if any real identifier it accepted still appears in the scrambled text.

What is missing: It is opt-in per document through its own endpoints (ordinary chat and drafting traffic does not pass through it), and it needs a local model host configured.

src/lib/scrambler/guard.tssrc/lib/scrambler/orchestrate.tssrc/app/api/v1/scramble/route.ts/scramble
Confidentiality · Privacy
Document text is treated as untrusted input, and never silently rewritten
live today

A file you upload is frequently not written by you. Text from one is scanned for instruction-override patterns before it reaches the prompt, characters that exist only to hide a payload are stripped, the rest is fenced verbatim, and what was found is reported so a person can look at the exhibit. Evidence is never altered or deleted.

src/lib/security/injection.ts/docs/concepts/case-files
Processing Integrity · Security
Unverifiable citations are removed and disclosed, and a verifier failure is never a verdict
live today

Every citation in an answer is checked against our own offline reporter table first, then a persistent verdict cache, then at most one batched external lookup. A rate limit, a timeout or an exhausted budget returns “unverified”, never “does not exist”. A quoted passage that is not verbatim in the text we hold is replaced in place by a line naming the citation and saying the rule may have been amended, because a superseded quotation and an invented one look identical to a text match and the disclosure has to say so.

src/lib/rag/citecheck.tssrc/lib/rag/quotegate.tssrc/lib/rag/citesupport.ts/check
Processing Integrity
Public responses carry no internals, and a test enforces it
live today

A keyless endpoint once served internal store notes to anonymous callers. A test now scans whole public response bodies for repository paths, internal hostnames, absolute paths on our machines, personal addresses and box IP addresses, and fails on any hit, while separately asserting the internal store still holds those notes, so only the published copy is scrubbed. The health endpoint likewise gives anyone a per-check boolean and gives raw error strings, which can name a database host, only to an operator holding the admin token.

tests/public-api-no-internals.test.tssrc/lib/contract-law/publicize.tssrc/lib/api-error.tssrc/app/api/health/route.ts/api/health
Security · Confidentiality
Nothing reaches production without a commit, a type check and the test suite
live today

The deploy script refuses an uncommitted working tree, type-checks, runs the whole suite and refuses on any failure, then stamps the commit into the build so /api/health reports the exact sha production is serving.

scripts/ship.shsrc/app/api/health/route.ts/api/health
Processing Integrity · Availability
A deploy that does not answer correctly rolls itself back
live today

The new build is staged beside the live one and swapped atomically. The script then requires an HTTP 200 on the public site and one real grounded query that returns verified citations; either one failing restores the previous build and restarts the app.

scripts/ship.shscripts/synthetic.ts
Availability · Processing Integrity
Health is probed against real dependencies, and published
live, with a gap

The health endpoint queries the database, runs a real retrieval query that must return hits, and asserts the citation library knows a real citation and does not know an invented one. A synthetic grounded query runs on a schedule and turns health degraded if it fails twice or stops reporting for 45 minutes.

What is missing: Nothing pages a human. The watcher writes a status file and appends to a log, and the alert fires only when the overall state flips, which is how a 17-day outage of the watcher itself went unnoticed. A real on-call path is the first item on the readiness gap list.

src/app/api/health/route.tsscripts/synthetic.tssrc/app/status/page.tsx/status
Availability
Secrets live in the host environment, never in the repository
live today

Every .env file is gitignored, and a pre-commit hook refuses any commit whose staged content matches a credential shape: provider keys, Stripe keys, webhook secrets, AWS ids, private-key blocks, GitHub and Slack tokens, or a database URL carrying a password. Master keys for case-file and provider-key encryption are read from the process environment and are never written to the database.

.gitignore.githooks/pre-commitsrc/lib/security/crypto.tssrc/lib/byok.ts
Security · Confidentiality
Retention is per file, and website analytics expire
live, with a gap

A case file's own retention period wins, then the host default; a purge job deletes what is past it and is dry-run unless told to apply. Website page-view rows are deleted after 90 days, and a Do Not Track or Global Privacy Control signal drops the referrer, the IP address, the resolved location and the visitor hash entirely.

What is missing: No host default retention is set today, so a file with no period of its own is kept until you delete it, and the case-file purge is run by hand rather than on a schedule.

src/lib/db.tsscripts/purge-case-files.tssrc/lib/pageview.ts/policy
Privacy · Confidentiality
Nightly off-host backups, and a law library archived object by object
live, with a gap

A nightly job copies the benchmark bank, the published splits and the local database fallback off the application host to a second machine, keeping fourteen days. Separately, all 36 retrieval shards are archived to object storage and were verified by listing the bucket and comparing every one of 77 objects against its local file byte for byte, not by trusting the archive script's exit code.

What is missing: Neither covers the application database that holds accounts, keys and usage. There is no automated backup of it today; that is the second item on the readiness gap list.

scripts/backup.shdocs/HOLDINGS.mddocs/RUNBOOK.md
Availability

What leaves this box, and to whom

A request to DocketRouter is one model call. We build the prompt and send it to one model provider through OpenRouter, on an allowlist of providers whose published policy is that they do not train on prompts. Nothing else sees it: citation checking runs against our own offline library, so the act of verifying an answer never discloses your citations, or the question behind them, to anybody. Retrieval, reranking and grading run on our own hardware.

Every call carries data_collection: "deny". A key set to no_retention also carries zdr: true, and if no zero-retention endpoint can serve the model the request fails with a 503, and we do not retry it on a provider that retains. A request can only ever narrow the key's policy; anything wider is a 400. We have not negotiated zero-retention contracts of our own: what we rely on is OpenRouter's published classification, and /policy quotes its own caveat about that verbatim.

The mitigation that does not depend on anyone's promise is the scrambler. It replaces client names, organisations and identifiers in a document with placeholders using a model on our own hardware, keeps the mapping on that machine, sends only the placeholder version onward, and restores the real names in the answer. A gate refuses to send the document at all if any identifier it accepted still appears in the scrambled text, so the failure mode is "nothing was sent", never "sent anyway". See /scramble.

If a key is attached to a private pod, none of the above applies: the model runs on hardware we control and the prompt does not leave it. On this host a pod endpoint is configured, and case-file encryption at rest is not enabled. Those two lines are read from this host at request time, not written into the build.

The full vendor list, naming what each one does for us, the file that calls it, and the environment variable without which it does not run at all, is section 6 of docs/SOC2-READINESS.md. Our hosting is rented hardware we administer ourselves: one application host, one database host, one retrieval host with a fallback. Single region, United States. There is no residency option today, and we do not pretend otherwise.

What we keep, and for how long

Prompts and answers: not stored. One usage row per request holds metadata only: key id, model, token counts, cost, latency, citation counts, status, a request id, the upstream generation id and the provider that served it. Prompt and answer text is stored only if you switch log_content on for a key, which is off by default; switch it off and nothing further is kept.

Files and matters: a file's own retention period wins, then the host default. No host default is set today, so a file with no period of its own is kept until you delete it, and deleting a matter deletes its files and its turns with it. The purge job is run by hand rather than on a schedule. Both of those are on the gap list rather than described as a policy.

Website analytics: first-party only, no cookies, no third-party script. One row per page load, deleted after 90 days. IP geolocation runs against a table on our own server, so your address is never sent to a geolocation service. If your browser sends Do Not Track or Global Privacy Control we still count the visit and drop the referrer, the address, the location and the visitor hash entirely.

The audit log is kept indefinitely, deliberately, and cannot be edited: database triggers raise on any UPDATE or DELETE against that table. The retention schedule for the usage table and for matter messages has not been written down yet, which is an honest gap and is listed as one. The whole of this section in detail: /policy.

Our corpus is public law, and your data is never in it

The library DocketRouter retrieves from is published law and our own benchmark, released as open datasets: 615,391 Texas appellate decisions, 122,681 statute sections, 45,660 Supreme Court opinions, the court rules we hold verbatim, and an 18-million-row citation existence table. Anyone can download it and check our answers against it.

No customer data is ever published, and none of it is ever added to that library. The shared library is read-only in the request path: your queries do not enter it, your files do not enter it, and nothing you send is used to train or tune anything. Your files are retrieved only back into your own requests, isolated by your account id in the query itself. What we publish is law, which was already public; what you send us stays yours.

Read from this repository on 2026-10-07. If a line here disagrees with the code, the code is right and this page is a bug. Tell us at hello@docketrouter.ai and quote the control. Related: how we operate, live status, terms.