chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
# @pdsjs/git-host
A git forge served from a PDS reached over the wire. The PDS stays the data
store and the identity provider: users log in with their atproto account,
repositories live in the account's PDS as `dev.pdsjs.git.repo` records with
bundle blobs, pushes go to the PDS exactly as the [git helper](../git/README.md)
makes them. What moves out is the hosting: this service reads the account's
records and blobs through the public XRPC surface and serves the forge from
its own origin.
```
git CLI ──────────────────► PDS (uploadBlob + putRecord, branch protection)
git-ui ◄──► git-host ─┬─ /git/ smart HTTP, raw files, the file browser
├─ dev.pdsjs.git.* reads, answered from the wire
├─ discovery: Jetstream → SQLite, no Constellation
└─ owner login: atproto OAuth, session cookie
PDS ── the data store; git hosting there becomes optional
```
## Running
```sh
git-host --pds https://pds.example.com --account alice.example.com \
--db ./forge.sqlite --port 8046 --origin https://git.alice.example.com \
--ui ./git-ui-dist
```
| Option | Purpose | Default |
| --- | --- | --- |
| `--pds` | the account's PDS base URL | required |
| `--account` | the hosted account's handle or DID | required |
| `--db` | the index database (SQLite) | `./git-appview.sqlite` |
| `--port` | the port to serve on | `8046` |
| `--origin` | the origin requests are read as carrying; required for login | host header |
| `--ui` | the git-ui build to serve at `/` | none |
| `--plc-url` | the directory DIDs resolve against | `https://plc.directory` |
| `--jetstream` | the Jetstream to index from (a v2 instance) | `wss://jetstream.us-east.bsky.network` |
| `--jetstream-api-key` | the archive's bearer token, for replay on a metered instance; also read from `JETSTREAM_API_KEY` | none |
| `--replay` | catch up from the Jetstream archive on boot instead of walking the relay; implies `--no-backfill` | off |
| `--list-service` | the relay the backfill lists repos from | `https://bsky.network` |
| `--no-watch` | skip the Jetstream tail | |
| `--no-backfill` | skip the first-boot historical walk | |
## What it serves
- `/git/<account>/<repo>` — the read-only smart HTTP endpoint and raw
files, exactly the surface the embedded extension serves. Stock git
clones through it with no helper installed.
- `/xrpc/dev.pdsjs.git.*` — the same XRPC methods the embedded extension
serves, answered from records and blobs fetched on demand. Record reads
are anonymous; the owner's writes carry the owner's OAuth token, which
the login flow below put in the session.
- `/.well-known/atproto-did` and `/.blobs/<cid>` — the two routes the
browser app reads from its own origin, so the app works served from the
forge without CORS negotiation.
- `/` — the [git-ui](../git-ui) build, when `--ui` names one.
## The index
Discovery on the PDS asks Constellation, a hosted reverse index, who wrote
about a repository. The forge asks its own index instead. Two ways to fill
it, one fold:
- **The archive, replayed.** Jetstream v2 keeps the whole network in
sealed segment files. With `--replay` the forge pages `planSnapshot`
for the `dev.pdsjs.git.*` collections, downloads the matching blocks
from the archive, folds them into SQLite, and cuts over to the live
tail at the sealed tip. A refused live cursor sends the tail back to
the archive for the gap. On Bluesky's public v2 instances the archive
HTTP is metered and needs `--jetstream-api-key`.
- **The relay walk, backfilled.** Without `--replay`, a first boot walks
`com.atproto.sync.listReposByCollection` and lists each account's
collections from their own PDS, so strangers' pull requests and issues
appear without waiting for the next live write.
The index is at-least-once by design: a record upserts by uri, a delete
removes, and replaying an event already indexed changes nothing.
## Login
`GET /login` starts an atproto OAuth authorization-code flow for the forge
owner: PKCE, a DPoP-bound token pair, and a session cookie. The client
metadata document lives at `/oauth/client-metadata.json`, the callback at
`/oauth/callback`. A session whose DID is not the hosted account's grants
nothing, because every owner-only procedure compares against the hosted
account. The default grant is the `atproto` scope, which covers the record
and blob writes the forge makes.
Collaborators keep pushing from their own accounts to their own PDSes; the
forge reads their copies from there, exactly as the [collaboration
model](../../docs/plans/2026-08-21-git-collaboration.md) describes.
Repositories held in spaces are not served by the forge yet: the space
credential chain needs a consented `space:` grant per member, which the
CLI helper holds and the forge does not yet ask for.