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.
Top-level declarations
Every Carrier program is composed from this set. No imports bring in new grammar; no macros hide behavior.
Types & annotations
Carrier has named records, enums, arrays, optionals, and a tight scalar set. Validation and DB metadata attach with @-annotations.
StringUTF-8 stringInt · Float64-bit integer / floating pointBoolbooleanUUIDUUIDTime · Datetimestamp · calendar dateJson · Voidarbitrary JSON · no valueT[]array of TT?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
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.
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.
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 }}exact, contains, and min today.active. Internal callers can pass all or deleted.Routes
Explicit HTTP. Typed path params, query blocks, input bodies, response types, auth forms, and optional idempotency.
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) }}public, protect Auth, protect Auth roles [admin, editor].input, query { }, and params { }.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.
/// Transaction-safe admin action with row lockingaction 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 responseroute POST "/admin/doctors/{id}/lock" protect AdminAuth roles [admin] idempotent -> Doctor{ params { id: UUID } handler { return audit_locked_doctor(params.id) }}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.
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}ENABLE ROW LEVEL SECURITY + FORCE ROW LEVEL SECURITY.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.
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.
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.
/// Admin reporting endpoint using raw SQL mapped into a typed result listroute 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.register(...)
- auth.login(...)
- auth.issue_tokens(...)
- auth.current_user()
- logs.debug(...)
- logs.info(...)
- logs.warn(...)
- logs.error(...)
- cache.get_as(type_name, key)
- cache.set(key, value, ttl_seconds: 120)
- cache.exists(key)
- redis.publish(channel, message)
- jobs.enqueue(name, payload, delay_seconds: 5)
- audit.record(action, entity, entity_id, metadata)
- sql.list_as(type_name, sql, ...)
- sql.one_as(type_name, sql, ...)
- db.fn_scalar_as(type_name, fn, ...)
- 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.
.carrier file under src/ still compiles together.return inside transaction { } is not supported yet.Json payload types.