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.

Under -multi-org, orgs, API tokens, and published rulesets are created and managed for you on a separate internal system, not through stylist serve itself. Your account contact provisions your org and issues API tokens; stylist serve only ever reads them to authenticate requests.

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 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
docker pull ghcr.io/lukedevops/stylist:latest
docker run -p 8420:8420 \
-v /srv/stylist/rulesets:/rulesets:ro \
-e STYLIST_API_TOKEN=… -e STYLIST_LICENSE=… \
ghcr.io/lukedevops/stylist:latest

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.