Why Pylon fits
- Postgres tables with typed columns and foreign keys map to Pylon entities with
typed fields and
field.idrelations. - Row Level Security policies map to per-entity policies, and Pylon evaluates them the same way (an existing-row read side and an incoming-write side).
- Supabase Realtime maps to reactive queries against a local replica.
- Supabase Auth (email/password, magic link, OAuth, anonymous) maps one-to-one to Pylon auth.
- Your schema and policies live in TypeScript, next to your app, not in SQL migration files and a separate RLS layer.
- Hosted server functions (
query/mutation/action) in TypeScript, instead of Deno Edge Functions andplpgsql. - Server-side rendering: file-based routes, one binary serving frontend and API.
- Pylon runs on SQLite or Postgres and deploys with one command, or self-hosts as a single binary.
Concept map
Migrating your schema
Each Postgres table becomes a Pylon entity. Columns become typed fields, and a foreign key becomes afield.id.
Supabase (SQL):
app.ts):
- Map Postgres types to Pylon:
text→field.string,int/bigint→field.int,numeric/float→field.float,timestamptz→field.datetime,jsonb→field.json,bool→field.bool. - A
references auth.users(id)foreign key becomesfield.id("User"). Other table references becomefield.id("ThatEntity"). - Postgres check constraints and defaults map to field modifiers (
.optional(),.unique(),.default(...),.defaultNow()).
Migrating your policies
Supabase Row Level Security and Pylon policies are the same idea: a per-table read side and a per-table write side. One difference to audit: RLS is off by default on a new Supabase table, so any table where you never enabled it is fully open through the API. Pylon denies by default with no enable step, so every entity needs an explicitpolicy(). List your tables and check which ones never
had RLS enabled, then decide whether each should be public or locked down.
Supabase (RLS):
policy(...)):
auth.uid()maps toauth.userId.auth.jwt()claims (roles) map toauth.roles/auth.hasAnyRole(...).using (...)is the existing-row side. It maps toallowRead, and toexisting.*in the update and delete policy.with check (...)is the incoming-write side. It maps toallowInsertandallowUpdate, evaluated ondata.*(the row being written).- To lock an immutable field, use
field.string().owner()instead of awith checkthat compares the new value to the old one. - A cross-table check (
exists (select 1 from members ...)) maps toexists(Member where orgId == data.orgId and userId == auth.userId).
Migrating reads and writes
Reads. Asupabase-js query becomes db.useQuery, which is reactive by default,
so you do not wire up a separate realtime channel.
.insert/.update/.delete become the client entity hook or a server
function.
.insert/.update/.delete do not return the
affected rows unless you chain .select(). Pylon’s ctx.db.insert returns the
new id, and db.useEntity paints the change locally.
Migrating your data
Supabase is Postgres, so export is standard SQL tooling. This is the easy part. 1. Export withpg_dump. Use the connection string from Dashboard → Connect.
INVALID_ID). Use one of two methods:
- Derive the id (simplest). Convert each UUID to a Pylon id with a fixed
function, and apply the same function to every foreign key.
sha1(uuid)gives 40 lowercase hex characters. Derived ids are not time-ordered, so sort by yourcreated_atfields. - Map the id. Generate a new Pylon id per row with
generateId()from@pylonsync/sync, keep an old-to-new map, and rewrite foreign keys in a second pass.
auth.users. Import each into your Pylon
User entity, keyed by email, and map the old user UUID to the new Pylon id the
same way you map foreign keys. Sessions and password hashes do not transfer, so
users sign in again with magic code or OAuth, where email is the stable key.
Migrating auth
Each Supabase auth method has a Pylon equivalent.- Magic link (
signInWithOtp({ email })) maps toPOST /api/auth/magic/sendthen/magic/verify. - OAuth (
signInWithOAuth({ provider })) 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). getUser()/onAuthStateChange→user.idmaps todb.useUser()on the client andctx.auth.userIdon the server.
What does not map cleanly
Decide on these before you commit:- Raw SQL and
rpc. Supabase lets the client run stored procedures and, through the SQL editor, arbitrary SQL. Pylon’s data access is through entities and server functions, not arbitrary SQL from the client. Moveplpgsqlfunctions and complex queries into a Pylonqueryormutation. - Database triggers. A
plpgsqltrigger that runs on write becomes logic inside themutationthat makes the write, or a scheduled job. - Postgres-only features. Views, materialized views, and extensions do not carry
over as-is. Pylon has its own vector search, so
pgvectormaps tofield.vectorandctx.db.vectorSearch. - RLS defaults. A table where RLS was never enabled was open; in Pylon it needs an explicit policy. Audit every table.
- Realtime scope. Supabase realtime subscribes to a table and a filter string. Pylon’s reactive queries subscribe to the query itself, so you do not re-derive relevance on the client.
Get started
- Create a new Pylon app:
npm create @pylonsync/pylon@latest my-app. - Turn each table into an
entity, and each RLS policy into apolicy(). Audit tables that had RLS off. - Run the
pg_dumpexport, 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.