Why Pylon fits
Firebase and Pylon share the shape of a backend: a data store, auth, server functions, file storage, and access rules. Most of your app maps directly:- documents with fields and references map to entities with typed fields and foreign keys;
- security rules map to per-entity policies, and both deny by default;
onSnapshotlive listeners map to reactive queries against a local replica;- Firebase Auth maps to Pylon auth (email/password, email link, OAuth, anonymous).
- Hosted server functions in TypeScript (
query/mutation/action), each in a transaction, instead of deploying Cloud Functions. - Server-side rendering: file-based routes, one binary serving frontend and API.
- Relational queries with
include, plus aggregates and vector search. - A single Rust binary you can self-host, or one-command Pylon Cloud.
Concept map
Migrating your schema
Firestore has no schema, so first read your data and write one. For each collection, declare a Pylon entity with typed fields. Turn a reference (or an id string) into afield.id("Target") foreign key.
Firestore — a posts collection, documents shaped like:
app.ts):
- Map Firestore field types to Pylon: string, number, boolean, and Timestamp map
to
field.string/int/float/bool/datetime. A Map (nested object) or Array maps tofield.json(). Geopoint and Bytes have no direct type: store a Geopoint asfield.json()({ lat, lng }) and Bytes as a base64field.string(). - A subcollection (
posts/{id}/comments) has no direct equivalent. Flatten it to a top-levelCommententity with apostId: field.id("Post")field. - Firestore’s composite-index requirements become
indexeson the entity.
Migrating your security rules
Both systems deny by default, so this is a translation, not a change of posture. Write apolicy() for every entity, because an entity with no policy is
blocked.
Firestore (firestore.rules):
policy(...) in app.ts):
request.auth.uidmaps toauth.userId.request.auth != nullmaps toauth.userId != null.resource.data.*(the current row) maps todata.*on reads, and toexisting.*on update and delete.request.resource.data.*(the incoming write) maps todata.*in the insert and update policy.get()/exists()cross-document checks map toexists(Entity where field == auth.userId).- To lock an immutable field (Firestore’s
request.resource.data.author == resource.data.author), declare the columnfield.string().owner(). It stampsauth.userIdon insert, rejects a false owner, and locks the value on update.
Migrating reads and writes
Reads. A Firestore query becomesdb.useQuery, which is reactive by default (no
separate onSnapshot).
setDoc/addDoc/updateDoc/deleteDoc become the client entity hook
or a server function.
writeBatch or runTransaction becomes one mutation handler (the whole
handler is a transaction) or a POST /api/batch call.
Migrating your data
Use the Admin SDK to read your data out. The managedgcloud firestore export
writes a LevelDB format meant for restore into Firestore or BigQuery, not for
loading into another database, so it is the wrong tool here.
1. Export with the Admin SDK. Firestore has no single “dump everything” call,
so enumerate collections and recurse into subcollections.
INVALID_ID). Use one of two
methods:
- Derive the id (simplest). Convert each Firestore id to a Pylon id with a fixed
function, and apply the same function to every reference.
sha1(firestoreId)gives 40 lowercase hex characters, so it fits Pylon’s format and needs no lookup table. Derived ids are not time-ordered, so sort by yourcreatedAtfields. - Map the id. Generate a new Pylon id per row with
generateId()from@pylonsync/sync, keep an old-to-new map, and rewrite references in a second pass.
field.id column.
3. Import into Pylon. The admin batch endpoint loads rows quickly and
bypasses per-entity policies. It needs an admin token. Send batches of a few
hundred rows.
firebase auth:export users.json)
and import each into your Pylon User entity, keyed by email. Sessions do not
transfer, so users sign in again. Password hashes do not transfer either; move
users to Pylon’s magic-code or OAuth sign-in, where email is the stable key.
Migrating auth
Firebase’s providers each have a Pylon equivalent.- Email link (
sendSignInLinkToEmail/signInWithEmailLink) maps to Pylon’s magic code:POST /api/auth/magic/sendthen/magic/verify. - OAuth (
signInWithPopup(new GoogleAuthProvider())) maps to a redirect toGET /api/auth/login/:provider?callback=<url>. Pylon supports Google, GitHub, Apple, Microsoft, and about 20 more providers. - Anonymous (
signInAnonymously) maps to a Pylon guest session (POST /api/auth/guest). onAuthStateChanged→user.uidmaps todb.useUser()on the client andctx.auth.userIdon the server.
What does not map cleanly
Decide on these before you commit:- Schemaless documents. Firestore lets each document be any shape. Pylon is
schema-first, so you define the fields. Keep genuinely freeform data in a
field.json()column, but an update replaces the whole field (there is no per-key merge likeupdateDoc’s dotted paths). - Subcollections. Flatten each subcollection to a top-level entity with a parent
field.idcolumn. There is no nested-collection path in Pylon. - Document references. A Firestore
Referencefield becomes a plain id string in afield.idcolumn. You resolve it withinclude, not a secondgetDoc. - Write triggers. Firestore’s
onDocumentWrittenreacts to any write on a path. Pylon has no per-write trigger. Do the derived work inside themutationthat makes the write, or use a reactive query or a scheduled job. - Field types with no Pylon equivalent: Geopoint (
field.json()as{lat,lng}) and Bytes (base64 in afield.string()). - Realtime Database. Its JSON-tree model is out of scope for this guide.
Get started
- Create a new Pylon app:
npm create @pylonsync/pylon@latest my-app. - Write a schema (
entityandfield) for each Firestore collection, and apolicy()for every entity. - Run the Admin SDK export, the conversion, and the
/api/batchimport against a staging app first. - Point the client at Pylon:
init, the auth calls,db.useQuery, anddb.useEntity. - Run
pylon deploy, or self-host the single binary.