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.
stylist serve -rulesets ./rulesets \ -addr :8420 \ -db stylist.db \ -log checks.jsonl \ -reload 30sEndpoints
Section titled “Endpoints”POST /v1/check
Section titled “POST /v1/check”{ "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.
POST /v1/rulesets/validate
Section titled “POST /v1/rulesets/validate”{ "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.
GET /healthz
Section titled “GET /healthz”Always 200, always unauthenticated - for load balancers and container probes.
GET /dash
Section titled “GET /dash”The dashboard, when a check store is configured.
Authentication
Section titled “Authentication”Set a bearer token to require Authorization: Bearer <token> on the
API (constant-time compared, /healthz exempt):
STYLIST_API_TOKEN=… stylist serve … # or -tokenClients pick the token up from STYLIST_API_TOKEN automatically. An
empty token disables auth (localhost/dev).
Personal login tokens
Section titled “Personal login tokens”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 mode
Section titled “Multi-org mode”-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:
stylist serve -rulesets ./rulesets -db 'postgres://…' -multi-orgEach 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):
-dbmust be apostgres://DSN: row-level security, and so real multi-org isolation, is Postgres-only. SQLite is rejected.- The database role
-dbconnects as must not be a Postgres superuser orBYPASSRLSrole. 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-organd-tokenare 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.
Control plane
Section titled “Control plane”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 judge
Section titled “Tone judge”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:
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.
Ruleset hot-reload
Section titled “Ruleset hot-reload”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.
Check log and store
Section titled “Check log and store”Every check is written twice:
- JSONL log (
-log, default stdout): one line per check, containing the full verdict plussource. Ship it to your log pipeline, or ignore it. - Check store (
-db): SQLite file orpostgres://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.
Deployment
Section titled “Deployment”docker pull ghcr.io/lukedevops/stylist:latestdocker run -p 8420:8420 \ -v /srv/stylist/rulesets:/rulesets:ro \ -e STYLIST_API_TOKEN=… -e STYLIST_LICENSE=… \ ghcr.io/lukedevops/stylist:latestThe 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.