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.
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 withdefineLexicons, 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.