Skip to navigation

Data dictionary

The complete inventory of what Writ keeps about you and your agents. If an object isn’t listed here, Writ doesn’t store it.

A standing rule: raw secrets are never stored. API keys, sponsor tokens, magic-link tokens, and TOTP backup codes are kept as SHA-256 hashes only. The raw value is shown exactly once, at creation, and then it’s gone. The one exception is the TOTP secret itself, which must be stored reversibly to verify codes (DynamoDB encrypts it at rest).

Tenant

Your account. One tenant per email address. Everything below belongs to a tenant.

FieldMeaning
idten_… — the tenant’s unique id
nameHuman-readable name (defaults to your email domain)
emailThe address the tenant was created from
planfree, or flipped by the Stripe webhook after payment
policyThe tenant’s policy (see Policy)
stripe_customer_idSet once a Stripe customer exists for this tenant
pending_welcomeTrue until the first API key + sponsor token are issued
totp_secretBase32 TOTP secret, set only after enrollment is confirmed
totp_enrolled_atUnix timestamp of confirmed enrollment; its presence means step-up is active
totp_pending_secretNot-yet-confirmed secret from POST /v1/totp/enroll; cleared on confirm
totp_backup_hashesSHA-256 hashes of the 10 single-use backup codes; a used code’s hash is removed
notify_step_up_emailtrue by default; set false to disable the rate-limited STEP_UP email
step_up_webhook_urlOptional HTTPS URL to POST on every STEP_UP
step_up_webhook_typegeneric (default) or slack; controls payload shape
created_atUnix timestamp

API key

Authenticates calls to the gate. Looks like writ_…. A tenant can have many.

FieldMeaning
key_hashSHA-256 of the key. The raw key is never stored
prefixShort non-secret display prefix (e.g. writ_AbC1) so you can tell keys apart in the portal
labelOptional label you set at creation
principal_idThe identity-directory principal (prn_…) the key is issued to. Keys that predate the directory were migrated to the tenant’s default principal
emailAddress it was issued for
tenant_idOwning tenant
planMirrors the tenant plan at issue time
checks_used / receipts_countUsage counters for the current period
period / usage_sinceThe billing period the counters belong to
created_atUnix timestamp
revoked_atSet when revoked. Revoked keys stop authenticating immediately; the record is kept

Lost your key? While signed in to the portal you can mint a new one (API keys → + New key) and revoke the lost one. No need to delete anything. Portal issuance requires your authenticator code once you’ve enrolled one (fail-closed: no new keys until enrollment).

The human half of the credential pair. Looks like writ_sp_…. One per tenant, minted when the tenant is created, shown once, never recoverable. Lost it? Rotate it from the portal (API keys → Rotate sponsor token): a signed-in session mints a new token and the old one dies immediately, with a ROTATE receipt in the audit log. The API key alone can never rotate it.

It authorizes the human-side operations: creating grants, revoking and reinstating principals. The API key says which tenant is asking; the sponsor token says a human approved this.

Principal (kill switch)

Not to be confused with Principals (identity directory) below. This is the kill-switch principal: an agent acting on someone’s behalf — a sponsor (the human or system that approves) plus an agent (the software doing the work), within a tenant. Kill-switch principals come into existence implicitly — the first time a grant names them — and can be revoked pre-emptively, which denies everything they do.

FieldMeaning
sponsor_idWho approved this agent’s actions
agent_idThe agent itself
tenant_idOwning tenant

Principals (identity directory)

Your tenant’s directory of identities — teams, services, agents. Every API key is issued to exactly one directory principal and carries its principal_id; every receipt snapshots the principal at write time. See Principals.

FieldMeaning
principal_idprn_… — the principal’s unique id, stable across renames
tenant_idOwning tenant; directory lookups are always tenant-scoped
nameDisplay name. Unique per tenant, case-insensitively
name_lowerLowercased name, used for the uniqueness check
disabledWhen true, every key of the principal gets DENY until re-enabled
created_atUnix timestamp
deleted_atSet on delete. The record stays forever so old receipts keep resolving; the name becomes reusable

Deletes are tombstones: a deleted principal stays resolvable by id, but no key can ever be issued to it again and its surviving keys are denied.

Policy

One tenant-default policy per tenant, plus optional per-environment overrides (dev, staging, prod) and per-principal overrides (one per identity-directory principal). Each policy object maps verbs to modes and carries its own integer version (starting at 1, bumped on every update). Overrides are sparse: a verb an override doesn’t name inherits downward at check time — principal → environment → tenant default.

ModeMeaning
allowThe check passes without further approval
denyThe check fails; the write must not run
require_grantThe check passes only with a live grant
step_upA human sponsor must approve via a grant first
FieldMeaning
policyIdStable id: <tenantId>#tenant, <tenantId>#env#dev, or <tenantId>#prn#<principalId> (serialized with a pol_ prefix on the API)
scopetenant, environment, or principal
environmentdev, staging, or prod — set only when scope is environment
principalIdSet only when scope is principal
versionMonotonic version; every successful PUT bumps it
verbsThe explicit verb→mode map for this policy object
updatedAtUnix timestamp of the last update

Resolution order (highest first): per-principal override → per-environment override → tenant default. An override on a disabled or deleted principal is inert — it never supplies a mode. There are no named policy sets yet.

The verb set starts with the built-ins fixed in the gate (money movement, identity, data changes, communications, infrastructure, and others) and grows with the tenant’s custom verbs — see Custom verbs. A policy may name a custom verb from the moment it is created; deprecated verbs — rename aliases and retired verbs — are rejected in new policies with the offending names listed, while environment- and principal-override maps keep entries for every registry name so a rename never silently drops a verb’s override.

Custom verb

One entry in the tenant’s verb registry (custom_verbs), created with POST /v1/verbs. Renames and deletes are soft — the record is never hard-deleted: the name stays reserved forever so old receipts keep their meaning.

FieldMeaning
nameLowercase domain.action name, 1–64 chars: letters, digits, dots, underscores; at least one dot; may not collide with a built-in verb or another registry name
descriptionWhat the verb means — human documentation, shown in the dashboard
statuslive, or deprecated once renamed or retired. A deprecated verb still resolves in checks, can never be used in a new policy, and its name can never be reused
renamed_toSet only on a rename alias: the canonical name the old name resolves to (surfaced as aliasOf on the API)
created_at / deprecated_atUnix timestamps

Grant

A short-lived permission slip minted by a human (sponsor token or dashboard) after a STEP_UP, or directly via POST /v1/grants.

FieldMeaning
idgrnt_…
sponsor_id / agent_id / verb / target / purposeExactly what is allowed. All five must match the check
ttl_secondsLifetime. Defaults to 90 seconds, max 3600 (1 hour)
expires_atUnix timestamp when it dies
spentSingle-use semantics: it can’t be reused for a different write
tenant_idOwning tenant
created_atUnix timestamp

Receipt

Written for every decision the gate makes — ALLOW, DENY, STEP_UP, REVOKE, PRINCIPAL_CREATE, PRINCIPAL_UPDATE, PRINCIPAL_DELETE, VERB_CREATE, VERB_UPDATE, VERB_DELETE, and others. Denials write receipts too: the refusal itself is auditable. Receipts are the audit log, and the audit log is the meter.

FieldMeaning
receiptIdrcpt_…
tenantIdOwning tenant
decisionThe gate’s answer
sponsorId / agentId / verb / target / purposeWhat was decided on. verb echoes exactly what the caller sent
resolvedVerbSet only when the check resolved the verb before evaluating policy: a deprecated custom verb (rename alias) or a case-only mismatch was normalized to the registry’s canonical name. The policy was evaluated for this verb; verb keeps the caller’s original
grantIdThe grant that authorized it, if any
principalId / principalNameThe identity-directory principal the acting key belonged to, snapshotted at write time. Renames and deletes never rewrite history — an old receipt always shows the name as it was when the decision was made
reasonHuman-readable reason for the decision
policyIdThe policy object that supplied the verb’s mode (null when no policy was evaluated, e.g. unknown-environment denials)
policyVersionThat policy’s version at decision time (null when no policy was evaluated)
prevHash / chainHashTamper-evident chain: each receipt stores the previous receipt’s chainHash (or GENESIS), and chainHash = HMAC(canonical fields + prevHash) with a tenant-specific key
createdAtUnix timestamp

Verify any receipt chain with GET /v1/receipts/verify or ./writ verify-chain.

Revocation

The kill switch. Revoking a principal denies every subsequent check for that sponsor/agent pair, immediately.

FieldMeaning
sponsor_id / agent_idThe principal being cut off
reasonWhy, in your words
revoked_atUnix timestamp
tenant_idOwning tenant

Reinstate undoes it; the principal can check again.

Single-use sign-in token, emailed to you. Stored as a SHA-256 hash with a 15-minute expiry; the raw token travels in the URL fragment so it never lands in server logs. The account-creation variant works the same way and creates the tenant only when clicked.

Rate limits

Unauthenticated endpoints are bounded before any account lookup, so the limits themselves can’t be used to probe for accounts: 30 magic-link requests per hour per IP, a per-email cooldown on magic links, and per-tenant caps on key creation.

Other stored objects

ObjectPurpose
GitHub installationTenant-scoped GitHub App install record (installation_id, tenant_id, status, pr_comments_enabled, check_fail_ungated).
Badge tokenPublic SVG badge credential (token_hash, tenant_id, label, created_at).
LeaseSingle-use gated-execution lease (lease_id, tenant_id, verb, target, purpose, expires_at).
Vault credentialEnvelope-encrypted system credential (system_id, tenant_id, version, status, created_at).
ManifestSigned CI attestation manifest (tenant_id, repo_id, manifest, signature, drift, created_at).