Status: Draft
This guide walks through adopting the Safety Net API base specs for a specific state implementation.
| Package | Description | CLIs |
|---|---|---|
@codeforamerica/safety-net-blueprint-contracts |
Base OpenAPI specs, overlay resolver, validation | safety-net-resolve, safety-net-design-reference |
@codeforamerica/safety-net-blueprint-mock-server |
Mock API server and Swagger UI for development | safety-net-mock, safety-net-swagger |
@codeforamerica/safety-net-blueprint-clients |
Postman collection and TypeScript client generation | — |
States install these packages as dependencies and point the CLIs at their resolved specs.
mkdir my-state-apis
cd my-state-apis
npm init -ynpm install @codeforamerica/safety-net-blueprint-contracts @codeforamerica/safety-net-blueprint-mock-server @codeforamerica/safety-net-blueprint-clientsmy-state-apis/
overlays/ # State-specific overlay files
modifications.yaml
resolved/ # Generated output (gitignored)
.env # Environment-specific values (gitignored)
package.json
Add resolved/ and .env to .gitignore:
resolved/
.env
{
"scripts": {
"resolve": "safety-net-resolve --base=./node_modules/@codeforamerica/safety-net-blueprint-contracts --overlays=./overlays --out=./resolved",
"resolve:prod": "safety-net-resolve --base=./node_modules/@codeforamerica/safety-net-blueprint-contracts --overlays=./overlays --out=./resolved --env=production --env-file=.env",
"validate": "node ./node_modules/@codeforamerica/safety-net-blueprint-contracts/scripts/validate-openapi.js --spec=./resolved --skip-examples",
"mock:start": "safety-net-mock --spec=./resolved",
"swagger": "safety-net-swagger --spec=./resolved",
"build": "npm run resolve && npm run validate"
}
}Use an exact version in package.json to control when you pick up base spec changes:
{
"dependencies": {
"@codeforamerica/safety-net-blueprint-contracts": "1.2.0"
}
}Overlays modify the base specs without forking them. Each overlay file uses the OpenAPI Overlay Specification 1.0.0 format.
A working example is included in the base repo at openapi/overlays/example/modifications.yaml. The patterns below reference that file — use it as a starting point for your own overlay.
overlay: 1.0.0
info:
title: My State Overlay
version: 1.0.0
description: State-specific modifications to the base Safety Net API schemas.
actions:
- target: <JSONPath expression>
file: <filename> # Optional: disambiguate when target matches multiple files
description: <what this action does>
update:
<replacement value>The --overlays directory is scanned recursively — any .yaml file starting with overlay: 1.0.0 is discovered and applied. You can organize overlays into multiple files if preferred.
Where a schema lives determines its JSONPath:
- Schemas in API spec files (e.g.,
persons.yaml,users.yaml): nested undercomponents/schemas, so the target starts with$.components.schemas. - Schemas in shared component files (e.g.,
components/common.yaml,components/auth.yaml): top-level in the file, so the target starts with$.
When a schema name appears in multiple files, use file: to specify which file the action targets. Without it, the resolver warns and skips ambiguous matches.
# Program exists in components/common.yaml — use file: to be explicit
- target: $.Program.enum
file: components/common.yaml
description: Replace program names
update:
- snap_state
- tanf_stateReplace enum values — change terminology to match state conventions:
# From the example overlay: replace citizenship status terminology
- target: $.components.schemas.CitizenshipInfo.properties.status.enum
file: persons.yaml
description: Replace citizenship status values with state-specific terminology
update:
- us_citizen
- lawful_permanent_resident
- qualified_alien
- prucol
- undocumented
- otherAdd properties — extend schemas with state-required fields:
# From the example overlay: add geographic tracking to Person
- target: $.components.schemas.Person.allOf.1.properties
file: persons.yaml
description: Add state-specific geographic tracking fields
update:
regionCode:
type: string
description: State region or county code for benefit administration.
example: "042"
regionName:
type: string
description: State region or county name.
example: "Example County"Replace a flexible schema — define explicit properties for schemas that use additionalProperties: true:
# From the example overlay: define UiPermissions structure
- target: $.components.schemas.UiPermissions
file: users.yaml
description: Add explicit property definitions to UiPermissions
update:
type: object
readOnly: true
additionalProperties: true
description: State-specific UI permissions.
properties:
availableModules:
type: array
items:
type: string
enum: [cases, tasks, reports, documents, admin, state_integration]
canApproveApplications:
type: booleanExtend auth context — add claims for state-specific authorization:
# From the example overlay: add program-based auth
- target: $.BackendAuthContext.properties
file: components/auth.yaml
description: Add program-based authorization to auth context
update:
programs:
type: array
items:
type: string
description: Programs the user is authorized to access.When multiple API versions exist (e.g., applications.yaml and applications-v2.yaml), use these optional properties to target a specific version or API:
- target: $.components.schemas.Person.allOf.1.properties
target-version: 2 # Only apply to v2 specs
description: Add field only to v2
- target: $.components.schemas.Application
target-api: applications # Match spec with info.x-api-id: applications
description: Target a specific APIOverlays customize OpenAPI specs (schemas, enums, endpoints). Behavioral artifacts — state machines, rules, metrics — use a different customization model: states provide their own YAML files that replace the baseline entirely.
The mock server discovers behavioral artifacts by file naming convention in the specs directory:
| Artifact | Convention | Example |
|---|---|---|
| State machine | {domain}-state-machine.yaml |
workflow-state-machine.yaml |
| Rules | {domain}-rules.yaml |
workflow-rules.yaml |
| Metrics | {domain}-metrics.yaml |
workflow-metrics.yaml (planned) |
To customize, place your own file with the same name in your state's resolved specs directory. It replaces the base file entirely — there is no merge.
The base workflow-state-machine.yaml defines 3 states and 4 transitions. States can replace this with their own lifecycle — adding states, transitions, guards, and effects.
Common customizations:
- Add states (e.g.,
awaiting_client,escalated,supervisor_review) - Add transitions between states (each trigger becomes an RPC endpoint automatically)
- Add or modify guards (preconditions for transitions)
- Add effects to transitions (
setfields,createaudit events,evaluate-rules)
See the Workflow domain for the full state machine architecture and effect types.
The base workflow-rules.yaml provides starter assignment and priority rules. States replace these with their own program-specific routing and priority logic.
Example: Adding a Medicaid queue and urgency-based priority:
ruleSets:
- id: workflow-assignment
ruleType: assignment
evaluation: first-match-wins
rules:
- id: snap-to-snap-queue
order: 1
description: Route SNAP tasks to SNAP intake.
condition:
"==": [{ "var": "task.programType" }, "snap"]
action:
assignToQueue: snap-intake
- id: medicaid-to-medicaid-queue
order: 2
description: Route Medicaid tasks to Medicaid intake.
condition:
"==": [{ "var": "task.programType" }, "medicaid"]
action:
assignToQueue: medicaid-intake
- id: default-to-general-queue
order: 99
description: Everything else goes to general intake.
condition: true
action:
assignToQueue: general-intakeKey points for rule authors:
- Conditions use JSON Logic syntax —
varreferences task fields (e.g.,task.programType,task.isExpedited) - Available actions:
assignToQueue(setsqueueIdby looking up a queue by name),setPriority(setspriorityfield directly) - Rules are evaluated in
order— lower numbers match first - Every rule set should end with a catch-all rule (
condition: true) to ensure a default is always applied
See the Workflow domain for the full rule architecture and how rules connect to the state machine.
Shared types (Address, Name, etc.) live in components/*.yaml and are referenced by multiple API specs via $ref. There are two approaches to customizing them:
Changes propagate to all specs that reference the type.
# Add a state-specific field to Address — affects all APIs
- target: $.Address.properties
file: components/contact.yaml
description: Add apartment/unit field to Address
update:
unit:
type: string
description: Apartment or unit number.Decouple from the shared type entirely. Use replace to swap a $ref with a state-specific inline schema.
# Replace the shared Address ref in Person with a state-specific schema
- target: $.components.schemas.Person.allOf.0.properties.address
file: persons.yaml
description: Use state-specific address format
update:
type: object
properties:
street1:
type: string
street2:
type: string
city:
type: string
state:
type: string
enum: [CA]
zipCode:
type: string
pattern: "^[0-9]{5}(-[0-9]{4})?$"Note: the components/ folder is preserved in resolved output — this is expected and harmless. Downstream tools consume the resolved API spec files, not the component files directly.
Tag spec sections with x-environments to include them only in specific environments:
# In your overlay or resolved spec
paths:
/debug/health:
x-environments: [development, staging]
get:
summary: Health check (non-production only)Resolve with --env to filter:
# Production: /debug/health is removed
safety-net-resolve --base=... --overlays=... --out=./resolved --env=production
# Development: /debug/health is kept, x-environments is stripped
safety-net-resolve --base=... --overlays=... --out=./resolved --env=developmentWithout --env, all sections are included as-is.
Use ${VAR} placeholders in string values for environment-specific configuration:
servers:
- url: ${API_BASE_URL}
description: API serverProvide values via .env file and/or environment variables:
# .env file
API_BASE_URL=https://api.example.gov
AUTH_ISSUER=https://auth.example.gov
# Resolve with substitution
safety-net-resolve --base=... --overlays=... --out=./resolved --env-file=.envEnvironment variables (process.env) take precedence over .env file values. This lets CI set overrides without modifying the file:
API_BASE_URL=https://api-staging.example.gov safety-net-resolve --base=... --out=./resolved --env-file=.envUnresolved placeholders produce warnings but don't fail the build.
A typical CI pipeline resolves overlays, validates, and generates artifacts:
# Example GitHub Actions workflow
name: Build and Validate
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
# Resolve overlays with production environment
- name: Resolve specs
run: npm run resolve:prod
env:
API_BASE_URL: ${{ vars.API_BASE_URL }}
# Validate resolved specs
- name: Validate
run: npm run validate
# Start mock server and run tests
- name: Integration tests
run: |
npm run mock:start &
sleep 3
npm testStates can declare how FK fields represent related resources. Set a global default in your config overlay and optionally override per field.
1. Set global style in config:
config:
x-relationship:
style: expand2. Add x-relationship to FK fields via overlay actions:
actions:
- target: $.components.schemas.Task.properties.assignedToId
file: workflow-openapi.yaml
description: Expand assignedToId with field subset
update:
type: string
format: uuid
description: Reference to the User assigned to this task.
x-relationship:
resource: User
fields: [id, name, email]When style: expand is set globally, individual actions only need resource and optionally fields. See the Relationship Configuration section in the overlays guide for full details.
The resolver applies transformations in this order:
- Copy base specs to output directory
- Apply overlay actions
- Resolve
x-relationshipannotations (if any FK fields are annotated) - Filter by
x-environments(if--envprovided) - Substitute
${VAR}placeholders (if--env-fileprovided or env vars exist)
When a new version of @codeforamerica/safety-net-blueprint-contracts is released:
- Review the changelog for breaking changes to schemas or file structure
- Update the dependency:
npm install @codeforamerica/safety-net-blueprint-contracts@<new-version> - Run resolve:
npm run resolve— overlay actions that target paths that no longer exist will produce warnings - Fix stale overlay targets: update JSONPath expressions to match the new schema structure
- Validate:
npm run validate— confirm the resolved output is valid - Run tests: verify your integration tests still pass
Pinning to exact versions (not ranges) gives you control over when to adopt changes.
- Keep
.envout of version control — add it to.gitignore - Keep
resolved/out of version control — it's generated output and may contain substituted secrets - Use CI environment variables for production secrets (API keys, auth issuer URLs) rather than committing them to
.envfiles - Review overlay changes — overlays can modify auth schemas, security schemes, and server URLs. Treat overlay changes with the same scrutiny as code changes.
Some state customizations may benefit all states. Consider proposing changes to the base specs when:
- A field is universally needed but missing from the base schema
- A pattern you've implemented via overlay would be cleaner as a base schema change
- You've identified a bug or inconsistency in the base specs
To contribute:
- Open an issue describing the proposed change and why it benefits multiple states
- If approved, submit a PR against the base
@codeforamerica/safety-net-blueprint-contractsrepo - Once merged, remove the corresponding overlay action — the change is now in the base