# Architecture

## Resource model

seekrit is multi-tenant. Resources nest from organizations down to individual secrets:

```
organizations ─┬─ members (users, via org_memberships with a role)
               ├─ groups ──────── environments ─┬─ secrets (+ version history)
               ├─ applications ─── environments ─┤   (shared, reusable secret bags)
               │        └─ composes groups        └─ key grants (wrapped DEKs)
               ├─ service tokens (bound to one app environment)
               └─ audit log (append-only)
```

- **Organization** — a tenant. Has members, applications, groups, service tokens, and its own
  audit trail.
- **Application** — a deployable (a service, site, or worker) within an org.
- **Group** — a reusable secret bag shared across applications (e.g. `common-backend`,
  `auth-providers`). Like an application, it owns environments keyed by slug.
- **Environment** — a named context (`production`, `staging`, `dev`, …) owned by **either** an
  application or a group. Each environment owns one data key.
- **Composition** — an application environment pulls in one or more groups, matched by slug. At
  read time the layers merge, lowest precedence first: `group secrets < app secrets`.
- **Secret** — a named, encrypted value in an environment. Every write appends a new version.
- **Key grant** — a wrapped copy of an environment's data key for one principal.
- **Service token** — bound to one application environment; that binding selects the org, app, and
  environment it resolves at runtime, plus the group slices composed into it.

## Roles

Membership carries a role: `owner` > `admin` > `member`.

- **Admin and owner** manage structure (apps, environments), service tokens, key grants, and can
  read the audit trail.
- **Members** read and write secrets in environments they hold a key for.

The real access boundary is cryptographic: you can only decrypt an environment if you hold a key
grant for it. Roles gate the management API on top of that. Non-members receive `404`s for an org
so its existence can't be probed.

## Cloudflare building blocks

seekrit is designed to run entirely on a Cloudflare account.

| Component | Cloudflare product | Role |
| --- | --- | --- |
| API | Workers | The Hono API worker |
| Database | D1 (SQLite) | Orgs, users, secrets (ciphertext), grants, audit |
| Cache | KV | Cached identity-provider JWKS (and future session/rate-limit state) |
| Web dashboard | Workers (via OpenNext) | Next.js app serving the browser client |

The API and web app deploy as separate Workers. D1 and KV are provisioned as bindings; the CLI
`wrangler` handles migrations and deploys.

## Request lifecycle

1. A request arrives at the API worker with a credential — a Stytch session JWT, a service token,
   or (locally) a dev-user header.
2. Auth middleware resolves the **actor** (a user or a service token) and, for org-scoped routes,
   checks membership and role.
3. The handler reads or writes ciphertext in D1. It never handles plaintext secrets.
4. Any mutating action writes an entry to the append-only audit log before the response returns.
   High-volume environment resolves are metered for usage/billing instead of audited per call
   (denied resolves are still audited).

## Authentication

- **Web** — Stytch B2B sign-in (Google/GitHub OAuth discovery). The browser holds the session;
  its JWT is sent as a bearer token and verified by the API against Stytch's JWKS (cached in KV).
- **CLI / machines** — service tokens (`skt_…`) sent as bearer tokens.
- **Local dev** — an `x-seekrit-dev-user` header, enabled only when the API runs with
  `AUTH_MODE=dev`.

The first time a valid session proves access to a Stytch organization, seekrit provisions the
matching org and membership just-in-time.
