Reference
Corporate best practices
Sanshain's rules are the same for one team and for two hundred. What changes at scale is how much of the setup is decided once, centrally, so that no repository has to be touched when the server moves, a token expires or a release policy changes. These are the practices that keep a large installation flexible, safe and maintainable.
What changed in 2.6.0
- Webhooks notify teams of publishes, rejections and cuts; receivers must be on an admin-managed allow-list.
- The catalog push also records the owner, description and links of a system, so the graph shows who owns what.
- Backup: a consistent SQLite snapshot on request and a Helm CronJob for either engine.
In short#
-
Environment over hardcoding: leave
sanshainUrlout ofsanshain.yaml; the URL and the token come from CI variables. -
Tokens with a scope and an expiry:
fullfor pipelines,read_onlyfor AI assistants, never in version control. -
Releases gated twice: the ga switch only on the protected-branch pipeline, the
releaserrole only on the accounts that pipeline uses. -
Honest versions: take the server's proposed version on a
409, never work around it. - The directory decides who is who: LDAP/AD or OIDC for people, groups mirrored to roles, a root account outside the database.
- Production infrastructure: PostgreSQL, backups, metrics, webhooks on an allow-list, a verified image.
Configuration from the environment#
Hardcoding the server URL in every project's sanshain.yaml means editing hundreds of
repositories when the instance moves. Every client reads the URL and the token from the
environment instead:
-
Leave
sanshainUrlout of the file (see top-level fields) and provideSANSHAIN_URLas an organisation- or group-level CI variable. -
Provide the token as
SANSHAIN_TOKEN, stored as a CI secret at the same level, so rotation is one change. -
For local development, developers set the two variables in their shell profile, or in the build
tool's own settings where the client supports it (Maven's
settings.xml; see Clients).
Tokens with a scope and an expiry#
Machine clients authenticate with an API token, a credential a user mints for themselves on the account page. Three properties matter at scale:
-
Expiry is mandatory. Every token is created with
expires_in_days, so a rotation policy is enforced by the service rather than remembered: pick the period your security policy prescribes (a year is common), and the token list shows when each token last authenticated a request, which finds the ones nobody uses. -
The scope matches the client. A
pipeline that provides and requires needs
full. An AI assistant on the MCP endpoint, a dashboard or a report job getsread_only: it answers reports and version listings and is refused with403on every route that changes the catalog. The scope is fixed at creation, so a leaked read-only token cannot be upgraded. -
A token carries its owner's permissions, no more. Publishing a GA needs the
owner to hold
release_ga; a token minted by an ordinary developer cannot release, whatever its scope. Mint the release pipeline's token from a releaser account and nobody else's.
Tokens never travel in a URL; the Authorization header is the only place the service
reads them from, so they do not end up in proxy logs or referrers. See
Using the API.
Releases gated by pipeline and role#
Sanshain never sees a git repository. Whether a build publishes a snapshot or a GA is declared by the build, and two independent gates keep that declaration honest:
-
The pipeline. Set the ga switch (
SANSHAIN_GA=true,--ga,-Dsanshain.ga=true) in the protected-branch pipeline and nowhere else. Every other build, CI or local, publishes a snapshot: overwritable, never compatibility-checked, expiring when unused. No branch detection, no per-project configuration; the pipeline is the release authority. See Stability from the pipeline. -
The role. A GA provide is refused unless the token's owner holds the
releaserrole (or is an admin), with the messagepublishing GA for '…' requires the 'releaser' role. Grant the role to the release pipeline's account, and a misconfigured feature branch cannot publish a GA even with the switch set. See Roles.
Consumers can opt into a snapshot by pinning its number; the dependency graph flags them as snapshot-pinned, so a release review can find every system that would ship against unreleased work.
Honest versions and breaking changes#
A pinned Consumer can never be broken by a new version; Sanshain's job is to keep the numbers
honest, so that a Consumer deciding on an upgrade can trust what they claim. A GA whose changes
are breaking without a major bump, or that reuses a number with different content, is refused with
409 and a proposed version.
- Take the proposed version and republish. Never work around a rejection by publishing a snapshot "for now" or deleting the old GA; the check caught a real forgotten bump.
- Publish breaking changes as a new major GA. The old majors remain immutable and keep serving their pinned Consumers; see API lifecycle for the deprecate-then-remove sequence.
- Use the graph's Outdated highlight and a Producer's dependents listing to see which teams still pin old versions, and talk to them before a removal.
- Deleting a GA version is an audited admin escape hatch, not a workflow step; do it only when the dependents listing shows no pinned Consumer. See Deleting a version.
Ownership from the catalog#
In a large graph the question is rarely "what depends on this" but "whom do I call". Let every
build push its Backstage catalog-info.yaml (catalogInfo in
sanshain.yaml, or PUT /catalog/{systemname}): the owner, description,
source location and links appear as the system's
catalog context on its page and in the
graph's node panel, and the databases, buckets and queues it lists under
dependsOn appear as
peripheral resources shared between the
systems that use them. Teams that already maintain a catalog file get this for free; for the
others, the file is a few lines. See
The catalog phase.
Notifications through webhooks#
Instead of polling the graph, let the service tell the teams: a webhook delivers signed events (a version published or deprecated, a provide rejected, a new Consumer recorded, a sanshain-branch cut, a catalog push) as a JSON envelope or a ready-made Slack or Teams message.
- The allow-list is the control point. A hook can only target a host an admin has put on the allow-list, so a hook created by a maintainer cannot exfiltrate catalog facts to an arbitrary URL. Keep the list to the corporate chat and ticketing hosts.
- Global hooks for platform teams, scoped hooks for product teams. A global hook with an event-type and Producer-glob filter feeds a platform channel; a Producer's own hook, managed by its maintainers, feeds the team channel; a Consumer's hook follows its require graph, so the team hears when something it pins changes.
- Verify the signature on the receiving side with the secret shown once at creation, and rotate it from the hook's page when a receiver changes hands.
Events are written to an outbox with the fact they describe and retried for 24 hours, so a receiver that is down over a weekend still gets its events. See Webhooks.
People from the directory, roles from groups#
- Delegate human login to the corporate directory: LDAP or Active Directory or OpenID Connect. Machine clients always use API tokens, whatever the mode.
-
Attach roles to
groups mirrored from the directory rather than to
individual accounts, so a team change in the directory is a role change in Sanshain. Keep
adminandreleaseron small, named groups. - Name maintainers per Producer: they manage its versions, retirements and webhooks without holding instance-wide permissions.
-
Keep a break-glass root account in
SANSHAIN_ROOT_USERS, held outside the database so that no stored grant can revoke it, and watch its use in the audit log.
Production infrastructure#
- PostgreSQL, managed, instead of the default SQLite: more concurrent writers and the backups you already have. Sanshain still runs as one instance; see Scaling.
-
Backups: the Helm chart renders a CronJob that stores a
pg_dump, or on SQLite fetches the consistent snapshot the service serves while running. The SQLite CronJob needs afull-scope token of an admin holdingrun_destructive_operations; aread_onlytoken is refused. Test the restore once; see Backup and restore. - Observability: scrape the Prometheus metrics (provides, requires, rejections, webhook deliveries and the outbox backlog) and export traces over OTLP; see Observability.
-
Egress: the service makes outbound calls only to the directory, the OIDC
provider, the OTLP collector and the webhook allow-list; a network policy can be that strict.
Private certificate authorities go through
EXTRA_CA_CERTS_DIR. - Supply chain: the image is signed with cosign and ships a CycloneDX SBOM; verify both before rolling out. See Verifying a release.
Related chapters#
- sanshain.yaml: the file every client reads.
- CI integration: tokens, stability and streams from the pipeline.
- API lifecycle: versions, deprecation, retirement.
- Authentication and Roles and permissions: who may log in and what they may do.
Checked against v2.6.0.