Carrier
Language reference

The Carrier language, grounded in what compiles today.

Fifteen top-level declarations, a small set of scalar and composite types, and a tight set of runtime-bound built-ins. Enough to build a typed API service without reaching for a framework. Narrow on purpose.

compiledtypedexplicit

Top-level declarations

Every Carrier program is composed from this set. No imports bring in new grammar; no macros hide behavior.

service
API surface & server settings
auth jwt
JWT issuer · audience · roles claim
import
validated path metadata
enum
named variants
type
request/response records
model
persisted records · CRUD · migrations
crud
generated resource endpoints
fn
pure reusable helpers
action
business logic with runtime context
policy
role/tenant visibility · RLS SQL
route
HTTP endpoints
client
typed outbound JSON integrations
event
durable domain events
job
async work units
schedule
cron-driven jobs

Types & annotations

Carrier has named records, enums, arrays, optionals, and a tight scalar set. Validation and DB metadata attach with @-annotations.

  • StringUTF-8 string
  • Int · Float64-bit integer / floating point
  • Boolboolean
  • UUIDUUID
  • Time · Datetimestamp · calendar date
  • Json · Voidarbitrary JSON · no value
  • T[]array of T
  • T?optional T
  • @length(min, max)string length bounds
  • @range(min, max)numeric bounds
  • @emailemail format validation
  • @primary · @uniqueidentity · uniqueness
  • @indexDB index
  • @belongs_to(Model)relation metadata
  • @versionoptimistic concurrency field
src/inventory/00_models.carrier
model · policy · crud
enum InventoryStatus { active archived }
policy InventoryItem {
write: roles [operator]
deleted: roles [operator]
}
model InventoryItem {
id: UUID
sku: String @length(min: 3, max: 80)
name: String @length(min: 3, max: 120)
warehouse: String
available_units: Int @range(min: 0, max: 1000000)
safety_stock: Int @range(min: 0, max: 1000000)
status: InventoryStatus = active
deleted_at: Time?
created_at: Time
updated_at: Time
}

Service & auth

One service per project. JWT auth configures issuance, verification, and role-aware route protection.

examples/hello-carrier/src/main.carrier
Tier 1 · hello-carrier
service App {
openapi { title: "Hello Carrier API" version: "0.1.0" }
server { host: env("HOST", "0.0.0.0") port: env_int("PORT", 3000) }
}
auth jwt Auth {
issuer: env("JWT_ISSUER", "carrier")
audience: env("JWT_AUDIENCE", "carrier-users")
secret: env("JWT_SECRET", "local-dev-secret")
}
type RegisterRequest {
email: String @email
name: String @length(min: 2, max: 100)
password: String @length(min: 8)
}
type AuthTokens { access_token: String refresh_token: String }
route POST "/auth/register" public -> AuthTokens {
input: RegisterRequest
handler {
let user = auth.register(
email: input.email, name: input.name, password: input.password
)
return auth.issue_tokens(user.id)
}
}
route GET "/me" protect Auth -> MeResponse {
handler { return auth.current_user() }
}

Models & CRUD

crud generates list · get · create · update · delete · restore for any model, with pagination, filters, sort, search, and soft-delete scopes.

api/src/main.carrier
generated endpoints
crud Doctor at "/doctors" {
create: protect AdminAuth roles [admin]
list: public
get: public
update: protect AdminAuth roles [admin]
delete: soft using deleted_at protect AdminAuth roles [admin]
restore: true protect AdminAuth roles [admin]
list_defaults {
page_size: 20
sort: "rating"
direction: desc
scope: active // active · all · deleted
}
filters {
specialty: exact
city: exact
language: contains
rating: min
}
searchable { full_name specialty city bio }
}
Filter modes
exact, contains, and min today.
Scopes
List/get default to active. Internal callers can pass all or deleted.
Audit
Generated create · update · delete · restore record audit entries automatically.

Routes

Explicit HTTP. Typed path params, query blocks, input bodies, response types, auth forms, and optional idempotency.

api/src/main.carrier
route GET "/doctors/{id}" public -> Doctor {
params { id: UUID }
handler { return Doctor.get(id: params.id) }
}
route POST "/admin/doctors/{id}/lock"
protect AdminAuth roles [admin] idempotent -> Doctor
{
params { id: UUID }
handler { return audit_locked_doctor(params.id) }
}
Auth forms
public, protect Auth, protect Auth roles [admin, editor].
Input
A route may declare any combination of input, query { }, and params { }.
Idempotency
With idempotent, replays with the same Idempotency-Key return the stored body without re-running the handler.

Actions & transactions

Move non-trivial writes out of routes. Actions run with auth context, can open DB transactions, and can lock rows.

api/src/main.carrier
row locking
/// Transaction-safe admin action with row locking
action audit_locked_doctor(id: UUID) -> Doctor {
let actor = auth.current_user()
transaction {
let locked = Doctor.get_for_update(id: id, scope: "all")
logs.info("doctor lock audit", {
action_name: "audit_locked_doctor"
doctor_id: locked.id
actor_email: actor.email
})
}
return Doctor.get(id: id, scope: "all")
}
/// Idempotent admin route; retries return the stored response
route POST "/admin/doctors/{id}/lock"
protect AdminAuth roles [admin] idempotent -> Doctor
{
params { id: UUID }
handler { return audit_locked_doctor(params.id) }
}
Transaction rule
transaction { } opens a real DB transaction. Success commits, failure rolls back. return from inside the block is not supported yet — do the work in the block and return after.

Policies & RLS

policy compiles into manifest metadata and PostgreSQL RLS SQL. The runtime sets role and tenant session values per request.

src/00_models/slots.carrier
tenant-aware policy
policy AppointmentSlot {
tenant_field: org_id
read: roles [viewer, scheduler]
write: roles [scheduler]
deleted: roles [scheduler]
}
model AppointmentSlot {
id: UUID
org_id: String @index
clinician_name: String @length(min: 3, max: 120)
starts_at: Time
status: SlotStatus = open
version: Int @version
deleted_at: Time?
created_at: Time
updated_at: Time
}
Database enforcement
Migrations emit ENABLE ROW LEVEL SECURITY + FORCE ROW LEVEL SECURITY.
Request-time context
Runtime sets carrier.current_roles and carrier.current_tenant on the Postgres session before DB work runs.

Jobs · events · schedules

Durable async edges of the service. One-parameter jobs return Void, events are recorded, schedules poll and enqueue.

src/15_async/jobs.carrier
cron · emit · enqueue
event SlotReminderQueued {
slot_id: UUID
trigger: String
}
job send_slot_reminder(payload: Json) -> Void {
logs.info("slot reminder job", { job_name: "send_slot_reminder", payload: payload })
audit.record("send_slot_reminder", "AppointmentSlot", "scheduled", payload)
}
schedule "*/15 * * * *" run send_slot_reminder
route POST "/slots/{id}/notify"
protect StaffAuth roles [scheduler] idempotent -> Json
{
params { id: UUID }
handler {
let job_id = jobs.enqueue("send_slot_reminder", { slot_id: params.id }, delay_seconds: 5)
emit SlotReminderQueued { slot_id: params.id, trigger: "manual" }
return { job_id: job_id }
}
}

External clients

Typed outbound JSON. Base URL, per-request headers, timeout, and a typed response mapping.

api/src/main.carrier
WeatherApi
client WeatherApi {
base_url: env("WEATHER_API_URL", "https://api.example.com")
header "x-api-key": env("WEATHER_API_KEY", "")
timeout_ms: 3000
}
type WeatherResponse {
city: String
temperature_c: Float
condition: String
}
route GET "/weather/{city}" public -> WeatherResponse {
handler {
return WeatherApi.get("/forecast", query: { city: params.city })
}
}

SQL & DB escape hatches

When model helpers aren't expressive enough — aggregates, joins, reports, or stored functions — sql.* and db.* map the result into a typed shape.

api/src/main.carrier
sql.list_as
/// Admin reporting endpoint using raw SQL mapped into a typed result list
route GET "/reports/doctors/by-city" protect AdminAuth roles [admin]
-> DoctorCityReportRow[]
{
handler {
return sql.list_as(
"DoctorCityReportRow",
"select city, count(*)::bigint as total from doctors where deleted_at is null group by city order by total desc, city asc"
)
}
}

Built-in namespaces

A compact set of runtime-bound namespaces covers the day-to-day backend surface. Each is typed, documented, and implemented today.

auth
  • auth.register(...)
  • auth.login(...)
  • auth.issue_tokens(...)
  • auth.current_user()
logs
  • logs.debug(...)
  • logs.info(...)
  • logs.warn(...)
  • logs.error(...)
cache · redis
  • cache.get_as(type_name, key)
  • cache.set(key, value, ttl_seconds: 120)
  • cache.exists(key)
  • redis.publish(channel, message)
jobs · audit
  • jobs.enqueue(name, payload, delay_seconds: 5)
  • audit.record(action, entity, entity_id, metadata)
sql · db
  • sql.list_as(type_name, sql, ...)
  • sql.one_as(type_name, sql, ...)
  • db.fn_scalar_as(type_name, fn, ...)
string · array · json
  • string.lower · trim · slug · contains
  • array.length · push · contains
  • json.parse · stringify · get

Current limits

Carrier is explicit about what it does not do yet. Everything here is intentional and documented.

Imports
Imports are validated metadata. They are not yet selective loaders — every .carrier file under src/ still compiles together.
Migrations
Structural diffing only. Renames are not inferred automatically.
Transactions
return inside transaction { } is not supported yet.
Scheduled jobs
Scheduled jobs currently enqueue with an empty JSON payload. Prefer Json payload types.