getting started
airspace uses an atproto account as the backend for your app: typed reads and writes, file uploads, login and a public API, with no database to run.
This page shows how to use airspace by building a sample app. New to atproto? Read the terms first.
what you need
Node 22 or newer, and an account on a PDS (personal data server) you can log into.
To run a PDS locally:
git clone https://github.com/danielroe/airspace
cd airspace
pnpm install
pnpm dev:pdsThat serves a PDS on http://localhost:2583 and prints credentials for two accounts. Keep it running.
To use your own account instead, create an app password in your account settings, and use https://bsky.social (or wherever your account lives) as the service. Spaces, at the end of this page, won't work there yet.
a project
mkdir notes && cd notes
pnpm init
pnpm pkg set type=module
pnpm add airspaceyour schemas
Every record needs a schema, called a lexicon:
// lexicons.ts
import { defineLexicons, field, space } from 'airspace/lexicon'
export default defineLexicons('dev.example', {
note: {
title: field.text({ max: 120 }),
body: field.markdown(),
createdAt: field.datetime(),
},
workspace: space(['note']),
})The first argument is your namespace: a domain you own, reversed. getair.space becomes space.getair, which sadly isn't that catchy. So note here defines the record type dev.example.note.
workspace declares a space, a private area for drafts, used at the end of this page.
your collections
Records of one type live in a collection:
// 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 },
})a client
Reads need no credentials, since a repo is public. Writes need a session:
// notes.ts
import { createAirspace, passwordSession } from 'airspace'
import { workspace } from './collections.ts'
const session = await passwordSession({
service: 'http://localhost:2583',
identifier: process.env.PDS_IDENTIFIER!,
password: process.env.PDS_PASSWORD!,
})
export const airspace = createAirspace({
// on a public PDS, your handle is enough: identity: 'you.example.com'
identity: { did: process.env.PDS_DID!, service: 'http://localhost:2583' },
spaces: { workspace },
session,
})read and write
// run.ts
import { airspace } from './notes.ts'
await airspace.notes.create({
title: 'Hello',
body: '# hello\n\nfrom my own repo.',
createdAt: new Date().toISOString(),
})
for (const note of await airspace.notes.list())
console.log(note.rkey, note.value.title)# `pnpm dev:pds` prints all three
PDS_DID=did:plc:... PDS_IDENTIFIER=alice.test PDS_PASSWORD=hunter2 node run.tsBoth notes are now in your public repo, which you can browse on pdsls.
your first draft
Anything in your repo is public straight away, which is awkward for a half-finished note. A space is a private area of the same repo, and publish() copies a record out of it.
Spaces are experimental. They need a PDS running prerelease software: atproto'spermissioned-databranch, or the@atproto/pdsspaces alpha. Hosted PDSes, includingbsky.social, do not support them yet. The API may change.pnpm dev:pdsruns one that does support them.
const draft = await airspace.workspace.notes.create({
title: 'Not public yet',
body: 'still writing',
createdAt: new Date().toISOString(),
})
await airspace.workspace.notes.publish(draft.rkey)
await airspace.workspace.supported() // check first, and hide the feature if falsenext steps
- Your content model: field types, relations and singletons.
- Reading and writing: queries, paging, caching and live updates.
- OAuth and permission sets: logging in as someone else.