Carrier
Part One · Why Carrier
Hands-on3 min read

Build in 30 Minutes

Reading about Carrier is one thing. Building with it is another. This interlude takes thirty minutes from a blank directory to a running service with a generated API contract, schema migrations, and a manifest you can govern.

The example is small but realistic — a tenant-scoped patient-intake service with one model, named types, an action, a route, and a policy. Adapt the field names and the domain language to your own use case as you go. If you are working with an AI agent, hand it this chapter and the construct selection guide in Appendix A; you will be reviewing source within minutes, not writing it.

0 Before You Begin (1 min)

You need a Carrier toolchain on your PATH. From the reference repository, build it once with Cargo:

cargo run -p carrierc -- --help

Or invoke carrier directly if it is already installed in your environment. Confirm it works before proceeding.

1 Scaffold the Project (2 min)

mkdir patient-intake

cd patient-intake

mkdir -p src/00_models src/10_types src/20_actions src/30_routes

Carrier follows the import graph rooted at src/main.carrier. We will create that file next, alongside one file per layer. The numeric prefixes are not cosmetic — they keep files sorted in dependency order: models before types, types before actions, actions before routes.

2 Declare the Service (3 min)

Open src/main.carrier and declare the service, the JWT auth scheme, and the import graph:

excerpt
service PatientIntake {
openapi {
title: "Patient Intake API"
version: "0.1.0"
}
server {
host: env("HOST", "0.0.0.0")
port: env_int("PORT", 4500)
}
}
auth jwt StaffAuth {
issuer: env("JWT_ISSUER", "carrier")
audience: env("JWT_AUDIENCE", "carrier-users")
secret: env("JWT_SECRET", "local-dev-secret")
roles_claim: "roles"
}
import "./00_models/intake_submission"
import "./10_types/intake_contracts"
import "./20_actions/submit_intake"
import "./30_routes/intake_routes"

3 Add a Model with Policy (5 min)

Open src/00_models/intake_submission.carrier. Notice how the model, the tenant scope, the role rules, and the PII annotation all live in the same place — and a reviewer can see in one glance that this is regulated, tenant-scoped, role-protected data:

enum IntakeStatus {

pending

in_review

completed

excerpt
}
model IntakeSubmission {
id: UUID
org_id: String @index
patient_email: String @email @pii(category: "contact")
status: IntakeStatus
notes: String?
version: Int @version
created_at: Time
updated_at: Time
}
policy IntakeSubmission {
tenant_field: org_id
read: roles [front_desk, clinician]
write: roles [front_desk]
}

4 Add Named Types (3 min)

Open src/10_types/intake_contracts.carrier. Named types give the API a shared vocabulary that survives schema evolution:

excerpt
type SubmitIntakeRequest {
patient_email: String @email
notes: String?
}
type SubmitIntakeResponse {
id: UUID
status: IntakeStatus
}

5 Add the Action (4 min)

Open src/20_actions/submit_intake.carrier. The action owns the business operation: identity capture, the transaction, the structured log, and the typed return. The route, in the next step, will stay thin:

excerpt
action submit_intake(payload: SubmitIntakeRequest) -> SubmitIntakeResponse {
let actor = auth.current_user()
transaction {
let submission = IntakeSubmission.create({
patient_email: payload.patient_email
notes: payload.notes
status: IntakeStatus.pending
})
logs.info("intake submitted", {
action_name: "submit_intake"
submission_id: submission.id
actor_id: actor.id
actor_email: actor.email
})
return {
id: submission.id
status: submission.status
}
}
}

6 Add the Route (2 min)

Open src/30_routes/intake_routes.carrier. The route is the HTTP edge — protected, role-restricted, typed in and out, and delegating immediately to the action:

excerpt
route POST "/intake/submit" protect StaffAuth roles [front_desk] -> SubmitIntakeResponse {
input: SubmitIntakeRequest
handler {
return submit_intake(input)
}
}

7 Verify and Generate (5 min)

Run the standard development loop:

excerpt
carrier fmt
carrier check
carrier build --target node
Then produce the governance artifacts:
carrier openapi > openapi.json
carrier migrate generate

8 Take Stock (5 min)

Look at what you have:

  • openapi.json — the public contract for POST /intake/submit, ready for the integration team
  • Generated SQL migrations — ready for DBA review before deployment
  • .carrier/manifest.json — machine-readable evidence of routes, models, types, and policies
  • A compiled Node service — ready to run against a Postgres sandbox

In thirty minutes, you have produced a service that obeys tenant scoping, role-based access, contract-first API design, and schema-aware migrations — without writing a controller, a validator, a serializer, or an OpenAPI annotation. Every file you touched is small, declarative, and reviewable.

Domain in. Service out. Same shape every time.

Part Two

Core Language Model

The constructs that carry architectural meaning

Contents