Skip to content

Server

stylist serve runs the core API: centralized rulesets, a verdict for every POSTed asset, and a record of every check. That record is the dashboard’s data feed. The same command backs hosted and self-hosted deployment; nothing in it assumes either.

Terminal window
stylist serve -rulesets ./rulesets \
-addr :8420 \
-db stylist.db \
-log checks.jsonl \
-reload 30s
{
"ruleset_id": "acme-brand-v1",
"asset": "docs/page.html",
"content": "<html>…</html>",
"source": { "adapter": "git-hook", "repo": "marketing-site" }
}

Returns the verdict (200), 404 for an unknown ruleset, 400 for a bad request. asset is a reporting label; the server never opens paths. source attributes the check in the log/store and is not echoed into the verdict.

Binary assets (images) are sent as content_base64 instead of content: raw image bytes are not valid JSON string data. The adapters switch automatically; hand-rolled clients should base64-encode anything that isn’t valid UTF-8.

{ "content": "id: acme-brand-v1\ncolors:\n allowed: [\"#1A2B3C\"]\n" }

Checks whether content (raw ruleset YAML) parses against the real schema. Always 200, with the answer in the body:

{ "valid": true, "errors": [] }

A ruleset that fails to validate is still a 200 — the request itself succeeded, the ruleset just isn’t valid. Only a malformed request body (not valid JSON) is a 400. Deliberately unauthenticated, like /healthz: it’s stateless and touches no store data, so there’s nothing a credential would protect. The control plane calls this when an admin publishes a ruleset.

Always 200, always unauthenticated — for load balancers and container probes.

The dashboard, when a check store is configured.

Set a bearer token to require Authorization: Bearer <token> on the API (constant-time compared, /healthz exempt):

Terminal window
STYLIST_API_TOKEN= stylist serve # or -token

Clients pick the token up from STYLIST_API_TOKEN automatically. An empty token disables auth (localhost/dev).

Alongside the shared token above, the server can also accept a personal token from someone who ran stylist login. See Personal attribution. This needs -oidc-cli-client-id (default $STYLIST_OIDC_CLI_CLIENT_ID), a separate, public OIDC client registration from the one dashboard SSO uses. Without it, a personal token on /v1/check is simply not accepted and the request falls back to whatever token auth is otherwise configured.

-multi-org accepts opaque per-org API tokens instead of the single shared -token, for hosted/multi-tenant deployments where different customers must never see each other’s data:

Terminal window
stylist serve -rulesets ./rulesets -db 'postgres://…' -multi-org

Each token is looked up (by SHA-256 hash) against the store’s api_tokens table, which resolves it to an org. Every check made with that token is recorded under that org. For tables covered by row-level security (see Database), it’s also isolated from every other org’s data at the database level, not just by application-level filtering.

Requirements, checked at startup (the server refuses to start rather than run with isolation silently not enforced):

  • -db must be a postgres:// DSN: row-level security, and so real multi-org isolation, is Postgres-only. SQLite is rejected.
  • The database role -db connects as must not be a Postgres superuser or BYPASSRLS role. Either unconditionally bypasses row-level security regardless of policy, so a superuser connection would look correctly configured while enforcing no isolation at all. Use a dedicated, ordinary role for the server’s runtime connection — never the admin/superuser account you migrate with.
  • -multi-org and -token are mutually exclusive. A static shared token authenticates with no org attached, which has no safe meaning once per-org tokens are in play. The server won’t start with both configured.

Orgs and API tokens are created and revoked through the control plane, a separate admin service. See below. The dashboard is org-isolated too, but only when -oidc-org-claim is set. See Sign-in and Database for what that requires. Without it, a signed-in dashboard user still sees every org’s data, the same as before multi-org mode existed.

Org creation, API token issuance/revocation, and ruleset publishing are handled by a separate service, controlplane, not by stylist serve itself. It’s a genuinely different binary (its own go.mod, joined to this repo only by a go.work file) that shares the same Postgres schema. It never mints tokens or publishes rulesets through stylist serve, and stylist serve never creates orgs. This keeps the two independently deployable: the control plane is an internal admin tool (create an org, mint or revoke a token, publish a ruleset), not something a customer ever talks to directly.

Terminal window
controlplane -db 'postgres://…' \
-base-url https://admin.stylist.internal \
-oidc-issuer https://your-idp.example \
-oidc-client-id stylist-controlplane-admin \
-admin-emails alice@yourco.com,bob@yourco.com \
-stylist-api-url https://api.stylist.internal
STYLIST_CP_OIDC_CLIENT_SECRET= # env-only
STYLIST_CP_SESSION_SECRET= # env-only, 32+ bytes, signs admin sessions

Sign-in is OIDC only, gated by -admin-emails (an allowlist of who is permitted in, since this UI can mint tokens for any org). There is no self-serve signup here: an admin creates an org, mints an API token for it, and publishes a ruleset for it (paste the same YAML a self-host ruleset file already has — the id: field inside names it), then hands the token to the customer. Self-serve org/user signup and billing are deliberately out of scope for now. See documents/status.md’s v2 backlog.

-stylist-api-url is optional: when set, it points at a running stylist serve instance, and publishing a ruleset validates it against the real schema first (via POST /v1/rulesets/validate) rather than only checking that it has an id: field. Left unset, or if that server is unreachable at publish time, publishing still works with just the basic check — schema validation is a defense-in-depth improvement, not a requirement.

tone: rules (see Rulesets) only actually run when the server has an LLM judge to consult. Otherwise they stay declared-but-skipped. Enable it with an Anthropic API key:

Terminal window
stylist serve -rulesets ./rulesets -anthropic-api-key sk-ant-... \
-tone-model claude-haiku-4-5
  • -anthropic-api-key (default $STYLIST_ANTHROPIC_API_KEY) — the judge is off when this is empty, which is the default. The key never reaches a client; it’s read once at server startup.
  • -tone-model (default $STYLIST_TONE_MODEL) — which model backs the judge. Defaults to a cheap/fast model (claude-haiku-4-5). A pass/fail classification against a short guidance prompt doesn’t need frontier-tier reasoning.
  • -anthropic-base-url (default $STYLIST_ANTHROPIC_BASE_URL) — override the API host, e.g. to point at a self-hosted proxy. Defaults to the first-party Anthropic API.

The judge is a straight text-in/text-out call (no tool use, no structured-output API) so alternative Provider implementations (self-hosted deployments on Bedrock, for instance) can slot in behind the same interface without changing anything else.

The rulesets directory is re-read at most every -reload (default 30s). Rule changes propagate to every adapter without restarts. A broken edit (invalid YAML, duplicate ID) does not take down checking: the server keeps serving the last good registry until the directory parses again. Startup still validates the directory so a misconfigured server fails fast rather than serving nothing.

Every check is written twice:

  • JSONL log (-log, default stdout): one line per check, containing the full verdict plus source. Ship it to your log pipeline, or ignore it.
  • Check store (-db): SQLite file or postgres:// DSN. This is what the dashboard queries. See Database.

A store write failure never fails the check: the verdict is already correct, and losing one dashboard row beats blocking a commit.

Terminal window
make docker
docker run -p 8420:8420 \
-v /srv/stylist/rulesets:/rulesets:ro \
-e STYLIST_API_TOKEN=… -e STYLIST_LICENSE=… \
stylist:VERSION

The image is distroless (no shell, no package manager), built from a static binary. For VPC/self-hosted customers this one container is the entire product: check API + rule registry + store + dashboard.