> Call POST /v1/check before a tool that writes. DENY means do not invoke the tool. For clean Markdown of any page, append .md to the page URL. For the complete index, fetch https://docs.withwrit.com/llms.txt.

# Self-hosting

> Run the full Writ gate inside your VPC or a container against your own DynamoDB-compatible endpoint. One code path with the managed gate — no feature drift.

# Self-hosting

The same `api.route` surface the managed Lambda serves ships in
`gate/selfhost.py`. Run it anywhere Python 3.12 runs, pointed at a
DynamoDB-compatible endpoint — real AWS inside your VPC, or DynamoDB-Local
for development. Tables are created on boot (idempotent, missing tables
only), and metered billing is off by design: on your infrastructure the
gate never talks to a Stripe price you don't control.

## Docker (fastest)

```bash
git clone https://github.com/AvenueDAdmin/writ
cd writ
WRIT_TOKEN_SECRET=$(python3 -c 'import secrets;print(secrets.token_urlsafe(48))') docker compose up
```

That boots `writ-gate` on [http://localhost:8787](http://localhost:8787) plus the
`amazon/dynamodb-local` container. Agents, the CLI, and the portal hit it
exactly like the managed gate:

```bash
WRIT_BASE=http://localhost:8787 writ check \
  --key writ_yourKey \
  --sponsor ops \
  --agent billing-bot \
  --verb db.write --target db!orders --purpose "place order"
```

## Configuration

| Env                                  | Meaning                                                                                                                                                  |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WRIT_DYNAMODB_ENDPOINT_URL`         | Point the gate's state at a different endpoint: a DynamoDB-compatible store inside your VPC, or the compose `dynamodb` service (`http://dynamodb:8000`). |
| `WRIT_TOKEN_SECRET`                  | **Required**. Boot refuses without it (self-hosting a gate whose tokens anyone could forge is worse than a down one).                                    |
| `WRIT_CHAIN_SECRET`                  | Audit-hash chain key. Falls back to `WRIT_TOKEN_SECRET`.                                                                                                 |
| `WRIT_SELFHOST`                      | `1` (default) forces metered billing off; `WRIT_FORCE_BILLING=1` on a staging clone re-enables the billing paths.                                        |
| `WRIT_CHECK_FAILURE_MODE`            | `failopen` (default) or `failclosed` — same semantics as the managed gate.                                                                               |
| `WRIT_POLICY_CACHE_TTL_SECONDS`      | In-process policy cache TTL, default `5` (0 disables).                                                                                                   |
| `WRIT_DYNAMODB_READ_TIMEOUT_SECONDS` | Southbound DynamoDB read budget, default `2` — the check decision deadline.                                                                              |

## What is the same, what is different

* **Same**: every `/v1` endpoint, receipts + audit hash chain, principals,
  policies and overrides, kill switch, GitHub App flow, and the
  infrastructure-failure envelopes.
* **Different**: metered billing (off), the managed portal login (portal
  routes serve, but Stripe wiring is absent), and the async GitHub scan
  worker — scans on a self-hosted gate run in-process rather than through
  a Lambda self-invocation.

## When to self-host rather than use the managed gate

* Compliance requires receipts and code never to transit Writ's AWS
  account.
* The gate must live on caller-side networking (VPC peering, private
  links) so the check decision never leaves your network.
* You want the metering contract replaced: your tenant rows, your
  quotas.