Swarf is a UCAN revocation service.
swarf serve --storage memoryBy default, Swarf uses PostgreSQL. Configure it with --postgres-dsn, a
config.yaml, or SWARF_STORAGE_POSTGRES_DSN. Configuration sections are
identity, server, log, and storage; environment variable names use the
SWARF_ prefix, for example SWARF_SERVER_PORT.
Publish a revocation with an issuer PEM key, the CID to revoke, and the delegation to revoke:
swarf revoke \
--issuer-key-file issuer.pem \
<revoke-cid> \
<delegation-or-container>delegation-or-container can be a file path or an encoded UCAN container
string. A file may contain either a CBOR-encoded delegation or a UCAN container
holding the delegation to revoke. When the issuer key issued the revoked
delegation, the delegation alone is enough; otherwise the container must also
include the delegation witnesses, from which Swarf builds the witness chain.
The service defaults to did:web:swarf.forgery.network at
https://swarf.forgery.network; override these with --service-id and
--service-url.
Retrieve a revocation’s DAG-JSON record with:
swarf get <revoke-cid>The command prints the service response and exits silently if the revocation is
not found. Override the service endpoint with --service-url.
Stream revocation DAG-JSON records as they arrive with:
swarf streamBy default, it starts from the current time. Pass --from 0 to stream all
records or --from <RFC3339 timestamp> to stream records recorded on or after
that time. Press Ctrl+C to stop streaming. Override the service endpoint with
--service-url.
The UCAN RPC endpoint. It supports /ucan/revoke; the invocation arguments
identify the revoked delegation and its delegation
path witness. These
delegations must be included in the invocation metadata. For example:
type RevokeArguments struct {
revoke Link
path [Link]
}
path proves the revocation issuer's authority over a delegation issued by
someone else further down a chain the issuer is involved in, and may be empty
when the revocation issuer issued the revoked delegation directly. The revoked
delegation itself must always be included in the invocation metadata.
Retrieves the most recent revocation for a delegation. It returns a DAG-JSON
record containing revoke (the delegation CID), cause (the CBOR-encoded
revocation invocation), and CBOR-encoded witness delegation blocks, or 404
when no revocation exists. For example:
{
"revoke": {"/": "bafyreiehytyi4q3t2amvf2abdlt5xnnqtaqkknf6yxhre4klpjnejlnsc4"},
"cause": {"/": {"bytes": "omF2AWNjYXBnL3VjYW4vcmV2b2tl"}},
"path": [
{"/": {"bytes": "omF2AWNjYXBsL3Rlc3QvaW52b2tl"}}
],
"recorded_at": "2026-07-17T09:00:00Z"
}A Server-Sent Events stream of compact DAG-JSON records. Each event has revoke
(the revoked delegation CID), path (the witness delegation CIDs), cause (the
revocation invocation CID), and recorded_at (the time the record was recorded). Use 0
to stream all stored records, or provide an RFC3339/RFC3339Nano timestamp cursor
to stream records recorded on or after it. The cursor is inclusive so consumers
resuming from the recorded_at of the last event they received do not miss
records that share it; deduplicate by the event id (the cause CID). For
example:
id: bafyreif5fzax7oygfafacvxq2ndhtkshz2av5m42hqeixea7giirdxe5dm
event: revocation
data: {"revoke":{"/":"bafyreiehytyi4q3t2amvf2abdlt5xnnqtaqkknf6yxhre4klpjnejlnsc4"},"path":[{"/":"bafyreiehytyi4q3t2amvf2abdlt5xnnqtaqkknf6yxhre4klpjnejlnsc4"}],"cause":{"/":"bafyreif5fzax7oygfafacvxq2ndhtkshz2av5m42hqeixea7giirdxe5dm"},"recorded_at":"2026-07-17T09:00:00Z"}Construct a client with the Swarf service DID and URL, and pass the issuer
revoking a delegation to each Publish call:
serviceURL, _ := url.Parse("https://swarf.example.com")
client, _ := swarfclient.New(serviceDID, *serviceURL)
// Revoke a delegation you issued directly.
err := client.Publish(ctx, revoker, revoked)
// Provide a witness path (root first) when revoking a delegation issued by
// someone else further down a chain you are involved in.
err = client.Publish(ctx, revoker, revoked, swarfclient.WithWitnessPath(path...))
record, err := client.Get(ctx, delegationCID)
for event, err := range client.Stream(ctx, time.Time{}) {
// event.Revoke, event.Path, and event.Cause are CIDs; event.RecordedAt is a time.
}Publish self-signs the revocation invocation with the passed revoker, which
must be the issuer of the revoked delegation or appear as an issuer in the
witness path provided with WithWitnessPath. Get returns a full
store.RevocationRecord; Stream returns compact api.FirehoseRevocation
values.
A push to main publishes to GHCR from the Container workflow. The prod
target becomes ghcr.io/fil-forge/swarf:main, a stripped binary on a slim
Debian base. The dev target becomes ghcr.io/fil-forge/swarf:main-dev and
adds delve plus a handful of debugging tools. Both cover linux/amd64 and
linux/arm64, and both also carry a sha-<short-sha> tag, the dev image with a
-dev suffix.
The same run asks infra-central to deploy the prod image. It dispatches a
bump-deployed-image event carrying the manifest digest it just pushed, and
infra-central's Bump deployed image workflow opens a pull request
pinning that digest in terraform/envs/dev/apps/terraform.tfvars, with
auto-merge enabled. infra-central's Check and deploy workflow runs
tofu apply on dev/apps on every push to its main, so merging that pull
request is what deploys.
The dispatch runs as the fil-forge-bot GitHub App and needs the
FORGE_BOT_APP_ID variable and the FORGE_BOT_PRIVATE_KEY secret. Prod pins
are promoted by hand.