The CMS you already have.

Every Bluesky* account comes with a personal data server: a database, login, file storage and a public API that you own and can move. airspace makes it the content layer for your site.

Try the demoRead the docs

pnpm add airspaceNode 22+. No codegen step.

Your model

Lexicons in TypeScript, under your own namespace.

// lexicons.ts
import { defineLexicons, field, space } from 'airspace/lexicon'

export default defineLexicons('dev.roe', {
  note: {
    title: field.text({ max: 120 }),
    body: field.markdown(),
    createdAt: field.datetime().optional(),
  },
  workspace: space(['note']),
})
// collections.ts
import { defineCollections, defineSpace } from 'airspace'
import lexicons from './lexicons.ts'

export const { note: notes } = defineCollections(lexicons, {
  note: { sort: [['createdAt', 'desc']] },
})

export const workspace = defineSpace(lexicons.workspace, {
  collections: { notes },
})

Your site

One typed client over your repo and your drafts.

// cms.ts
import { createCMS } from 'airspace'
import { workspace } from './collections.ts'

export const cms = createCMS({
  identity: 'roe.dev',
  spaces: { workspace }, // its `notes` is a public collection too
  session, // omit for read-only
})

const published = await cms.notes.list({ limit: 5 })

const draft = await cms.workspace.notes.create({
  title: 'Not public yet',
  body: '# hello',
})
await cms.workspace.notes.publish(draft.rkey)

What you get

Your content, your account
Records live in your repo under your own schema. Switch tools or hosts and nothing moves.
No database to run
Reads, writes, auth and images all go to the PDS. airspace is a typed client, not a service.
Drafts built in
Permissioned spaces hold what isn't public yet. Same API, one call to publish.
Typed from the schema out
Define the model once; records, keys, joins and OAuth scopes are inferred.

Works with what exists

airspace manages any collection you have a lexicon for, not only the ones you wrote.

  • com.whtwnd.blog.entryWhiteWind posts, drafted in a space and published to your public repo.
  • site.standard.*Publications and documents, read from a live repo.
  • community.lexicon.calendar.*Events and RSVPs, joined across two accounts.
  • your.own.lexiconWritten with defineLexicons, or brought in as JSON.

Nothing in your repo carries an airspace $type.

If you already know atproto

airspace sits between @atproto/lex-schema and your site. Lexicons are TypeScript; your public repo and your permissioned spaces share one typed API; airspace lexicons emit writes the JSON when you want to publish your schemas. It defines no content model, renders nothing and hosts nothing.

alpha Spaces need a PDS on atproto's permissioned-data branch, which hosted PDSes are still rolling out. The API will change.

* Bluesky is one of many apps that work with an atmosphere account.