<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Johnson Fash</title>
    <description>The latest articles on DEV Community by Johnson Fash (@johnsonfash).</description>
    <link>https://dev.to/johnsonfash</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F618913%2F523c9246-8eb5-463b-b193-4ebdf68a5c3f.png</url>
      <title>DEV Community: Johnson Fash</title>
      <link>https://dev.to/johnsonfash</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/johnsonfash"/>
    <language>en</language>
    <item>
      <title>The column that existed on staging and not on production</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:40:46 +0000</pubDate>
      <link>https://dev.to/johnsonfash/the-column-that-existed-on-staging-and-not-on-production-36le</link>
      <guid>https://dev.to/johnsonfash/the-column-that-existed-on-staging-and-not-on-production-36le</guid>
      <description>&lt;p&gt;The deploy was green. Both jobs, both environments, nobody paged.&lt;/p&gt;

&lt;p&gt;The column was on staging. It was not on production.&lt;/p&gt;

&lt;p&gt;It had gone out in a pull request that also touched nine files of application&lt;br&gt;
code, and the reviewer read the application code, because that's where the&lt;br&gt;
interesting part was. The schema change was three words. The deploy step that&lt;br&gt;
would have applied it never ran on production — somebody had moved that step a&lt;br&gt;
year earlier, and nothing had needed it since.&lt;/p&gt;

&lt;p&gt;Production found out at the first write, at eleven at night, and nobody could&lt;br&gt;
tell me whether that invoice had saved.&lt;/p&gt;

&lt;p&gt;Nothing about that is exotic. It's the ordinary failure mode of schema work,&lt;br&gt;
and it has nothing to do with how nice your query API is. I spent months on&lt;br&gt;
&lt;code&gt;findMany&lt;/code&gt;. The thing that costs money is what happens the day you change a&lt;br&gt;
column on a table that already has rows in it.&lt;/p&gt;

&lt;p&gt;So: the unglamorous half.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;forge push&lt;/code&gt; — the boring one
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge push
&lt;span class="c"&gt;# → [forge:push] applied 1, skipped 47, failures 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It reads your schema, introspects the live database, brings the database&lt;br&gt;
forward. Run it again and it does nothing — &lt;code&gt;applied 0, skipped 47&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important property is what it refuses to do. &lt;code&gt;push&lt;/code&gt; will not drop a column&lt;br&gt;
you removed from the schema, will not drop a table, will not change a column's&lt;br&gt;
type, and will not rename anything. It's additive by construction, which is&lt;br&gt;
what makes it safe to wire into a container's start command and stop thinking&lt;br&gt;
about. Destructive operations live somewhere you have to look at them.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;forge diff --check&lt;/code&gt; — the gate
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;diff&lt;/code&gt; is the read-only sibling — introspect, compare, print, never write — so&lt;br&gt;
CI can preview production with a read-only role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge diff &lt;span class="nt"&gt;--json&lt;/span&gt;          &lt;span class="c"&gt;# a DriftReport for jq&lt;/span&gt;
npx forge diff &lt;span class="nt"&gt;--check&lt;/span&gt;         &lt;span class="c"&gt;# exit 3 on drift&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exit codes matter more than they look. &lt;code&gt;--check&lt;/code&gt; exits &lt;strong&gt;3&lt;/strong&gt; for real drift&lt;br&gt;
and &lt;strong&gt;1&lt;/strong&gt; for "couldn't run at all": no &lt;code&gt;DATABASE_URL&lt;/code&gt;, unreachable database,&lt;br&gt;
schema didn't load. A job that fails identically on both trains everyone to&lt;br&gt;
ignore it.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;forge generate&lt;/code&gt; — a migration with no database in the room
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;forge diff apply&lt;/code&gt; generates by introspecting &lt;code&gt;DATABASE_URL&lt;/code&gt;. That's right for&lt;br&gt;
adopting a database somebody else created, and wrong for everyday work. CI has&lt;br&gt;
no database, so nothing can verify that a schema change arrived with its&lt;br&gt;
migration. Two developers generate against two local worlds, each file correct&lt;br&gt;
relative to a state that stops existing at the merge. And the same schema&lt;br&gt;
doesn't reliably produce the same SQL, so a reviewer can't regenerate one to&lt;br&gt;
check it against the change in the same pull request.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;forge generate&lt;/code&gt; diffs the schema against the &lt;strong&gt;last committed snapshot&lt;/strong&gt;&lt;br&gt;
and connects to nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge generate &lt;span class="nt"&gt;--name&lt;/span&gt; add-org-slug
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;migrations/
  meta/_journal.json          ordering
  meta/0002_snapshot.json     the schema's shape after 0002
  0002_add-org-slug.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A snapshot is what &lt;code&gt;introspect()&lt;/code&gt; would return if this schema were applied.&lt;br&gt;
Once that's a file, the differ doesn't care that it didn't come from a socket.&lt;/p&gt;

&lt;p&gt;Which unlocks the check a database-backed generator cannot perform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge generate &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[forge:generate] 1 change(s) are in the schema but not in any migration.
  - add orgs.region
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 3, in a pipeline with no database in it. Commit the &lt;code&gt;.sql&lt;/code&gt; and its&lt;br&gt;
snapshot together: the SQL says what will run, the snapshot says what the&lt;br&gt;
schema will then be. One without the other is half a change.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;forge migrate status&lt;/code&gt; — the two states nobody reports
&lt;/h2&gt;

&lt;p&gt;Everything above compares &lt;em&gt;intent&lt;/em&gt; — your schema against a snapshot, or against&lt;br&gt;
what a database reports. &lt;code&gt;migrate status&lt;/code&gt; compares &lt;em&gt;reality&lt;/em&gt;: the files in&lt;br&gt;
&lt;code&gt;migrations/&lt;/code&gt; against the rows in &lt;code&gt;_forge_migrations&lt;/code&gt;. It's the one command&lt;br&gt;
that genuinely needs &lt;code&gt;DATABASE_URL&lt;/code&gt;, because a database is the only thing that&lt;br&gt;
knows what it has run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ✓ 0002_add-org-slug.sql       2026-08-19T16:40:55Z
  ! 0003_alice-adds-note.sql    OUT OF ORDER — sorts before
                                0004_bob-adds-tier.sql, which is already applied
  · 0005_add-index.sql          pending
  ? 0006_from-a-branch.sql      NOT IN THIS CHECKOUT   applied 2026-09-02
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applied and pending, every migration tool shows. The other two are where&lt;br&gt;
production goes wrong, and no tool I know of — drizzle-kit included — reports&lt;br&gt;
either.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OUT OF ORDER.&lt;/strong&gt; Alice generates &lt;code&gt;0007&lt;/code&gt; on Monday. Bob generates &lt;code&gt;0008&lt;/code&gt; on&lt;br&gt;
Tuesday. Bob's merges first and ships. When Alice's merges, a migrator walking&lt;br&gt;
forward from the highest applied entry steps over &lt;code&gt;0007&lt;/code&gt; in silence. It is&lt;br&gt;
never applied at all, and nothing ever says so. You find out a fortnight later,&lt;br&gt;
as &lt;em&gt;"why is this column missing in staging"&lt;/em&gt;. The fix is to delete the file and&lt;br&gt;
its snapshot, pull, and regenerate — which also re-diffs it against the schema&lt;br&gt;
as it now is, because &lt;code&gt;0007&lt;/code&gt; was written against a world where &lt;code&gt;0008&lt;/code&gt; hadn't&lt;br&gt;
happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NOT IN THIS CHECKOUT.&lt;/strong&gt; The database has applied a migration your folder&lt;br&gt;
doesn't contain: somebody ran a branch against it. The schema in front of you&lt;br&gt;
is not the schema that database has, so everything you generate from here is&lt;br&gt;
built on a state you cannot see — correct against your snapshot, wrong against&lt;br&gt;
that database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx forge migrate status &lt;span class="nt"&gt;--check&lt;/span&gt;   &lt;span class="c"&gt;# exit 4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 4 is distinct from &lt;code&gt;generate --check&lt;/code&gt;'s 2 and 3, so a pipeline can say&lt;br&gt;
which gate refused it. Point it at staging; an empty per-PR database has&lt;br&gt;
nothing to disagree about.&lt;/p&gt;
&lt;h2&gt;
  
  
  The two refusals
&lt;/h2&gt;
&lt;h3&gt;
  
  
  A rename is not a drop and an add
&lt;/h3&gt;

&lt;p&gt;Comparing two schema states shows only that one name is gone and another has&lt;br&gt;
appeared. A rename and a drop-plus-add look identical from there, and they do&lt;br&gt;
opposite things to the data: one keeps every row, the other deletes a column's&lt;br&gt;
worth of it.&lt;/p&gt;

&lt;p&gt;Guessing "drop and add" loses data on a column somebody meant to keep.&lt;br&gt;
Guessing "rename" is worse — it keeps a column somebody meant to delete and&lt;br&gt;
quietly moves its data under a new name. So forge takes the answer from the&lt;br&gt;
schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;renamedFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;full_name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- up&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"orgs"&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"full_name"&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the annotation, a same-typed drop-and-add on one table is refused,&lt;br&gt;
naming both columns and printing the line to add. &lt;code&gt;--allow-drop&lt;/code&gt; is how you say&lt;br&gt;
&lt;em&gt;"it really is a drop, I mean to lose it"&lt;/em&gt; — deliberately a flag and not a&lt;br&gt;
prompt, so it lands in the shell history.&lt;/p&gt;

&lt;p&gt;drizzle-kit asks this interactively. Right question, wrong medium: a prompt&lt;br&gt;
answered once at 2am is recorded nowhere, cannot run in CI, and is invisible in&lt;br&gt;
review. An annotation is in the schema, the diff and the pull request.&lt;/p&gt;
&lt;h3&gt;
  
  
  A widening is emitted; a narrowing is refused
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;varchar(64)&lt;/code&gt; → &lt;code&gt;varchar(255)&lt;/code&gt;, &lt;code&gt;int&lt;/code&gt; → &lt;code&gt;bigint&lt;/code&gt;, dropping &lt;code&gt;NOT NULL&lt;/code&gt;: every&lt;br&gt;
existing row still fits, so the statement cannot fail on data. Emitted — with a&lt;br&gt;
&lt;code&gt;down&lt;/code&gt; that says out loud that reversing it can fail on rows written since. A&lt;br&gt;
file that says "rollback" without saying that is lying to whoever runs it at&lt;br&gt;
3am.&lt;/p&gt;

&lt;p&gt;Everything else is refused, with the migration to write instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✖ orgs.name: text → int
  orgs.name changes from text to int, which is not a widening — existing
  rows may not fit, or may not convert at all.
  → forge will not guess at this. Write it with `forge generate
    --custom`: add the new column, backfill it with whatever conversion
    is correct for YOUR data, verify, then drop the old one and rename.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit 2, and nothing is written — including the safe changes in the same diff.&lt;br&gt;
A migration that applies cleanly while leaving the schema and the database&lt;br&gt;
disagreeing is the failure this exists to remove, not a smaller version of it.&lt;br&gt;
&lt;code&gt;NULL&lt;/code&gt; → &lt;code&gt;NOT NULL&lt;/code&gt; is refused too, and the message says why the obvious fix&lt;br&gt;
doesn't work: a &lt;code&gt;DEFAULT&lt;/code&gt; applies to new rows, not to the NULLs already there.&lt;/p&gt;

&lt;p&gt;The refusal is the feature. A tool that emits &lt;code&gt;ALTER COLUMN … TYPE int&lt;/code&gt;&lt;br&gt;
against a column holding text is more dangerous than one that emits nothing,&lt;br&gt;
because the migration &lt;em&gt;looks reviewed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And before any of it, &lt;code&gt;npx forge doctor&lt;/code&gt; — driver inventory, a redacted&lt;br&gt;
&lt;code&gt;DATABASE_URL&lt;/code&gt; check, a schema lint, and a live probe that tells you &lt;code&gt;pg_trgm&lt;/code&gt;&lt;br&gt;
isn't installed before a GIN index tells you at deploy time.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two bugs, and one embarrassment
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The empty upsert
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The ordinary "insert if it isn't there, otherwise leave it alone" idiom — the&lt;br&gt;
first thing anyone writes for an idempotent seed. It compiled to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;CONFLICT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt;  &lt;span class="n"&gt;RETURNING&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SET&lt;/code&gt; with nothing after it. And because a parser blames the token &lt;em&gt;after&lt;/em&gt; an&lt;br&gt;
empty clause, every dialect reported &lt;code&gt;near "RETURNING": syntax error&lt;/code&gt; —&lt;br&gt;
pointing squarely at the one part of the statement that was fine.&lt;/p&gt;

&lt;p&gt;An empty set now emits a column assigned to its own stored value: valid SQL,&lt;br&gt;
provably no change, and &lt;code&gt;RETURNING&lt;/code&gt; still yields the row, which upsert's&lt;br&gt;
contract requires. &lt;code&gt;DO NOTHING&lt;/code&gt; would have parsed and is the shorter fix, but it&lt;br&gt;
returns no row on conflict — so &lt;code&gt;upsert&lt;/code&gt; would have resolved to &lt;code&gt;undefined&lt;/code&gt;&lt;br&gt;
exactly when the record already existed. A silent wrong answer in place of a&lt;br&gt;
loud syntax error is not a fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  The MySQL near-miss
&lt;/h3&gt;

&lt;p&gt;This is the part that stayed with me.&lt;/p&gt;

&lt;p&gt;MySQL rewrites each upsert assignment to &lt;code&gt;col = VALUES(col)&lt;/code&gt; so the update&lt;br&gt;
reuses the INSERT's values. Right for a real update. Catastrophic for the&lt;br&gt;
no-op, because &lt;code&gt;VALUES(`id`)&lt;/code&gt; is the id the INSERT &lt;em&gt;proposed&lt;/em&gt;. On conflict&lt;br&gt;
that would have silently replaced the existing row's primary key with a freshly&lt;br&gt;
generated uuid, taking every foreign key that referenced it along with it.&lt;/p&gt;

&lt;p&gt;No error. No failed statement. A row that still exists, under a new identity,&lt;br&gt;
with its children pointing at nothing. On a table of invoices that isn't a bug&lt;br&gt;
report, it's an incident. Caught before it shipped, by a hair.&lt;/p&gt;

&lt;h3&gt;
  
  
  The examples that quietly rotted
&lt;/h3&gt;

&lt;p&gt;Both were found by &lt;em&gt;running&lt;/em&gt; an example rather than reading one — which is the&lt;br&gt;
embarrassing bit. The examples lived in their own repository, pinned at&lt;br&gt;
&lt;code&gt;^2.5.6&lt;/code&gt; while the library reached 2.13.0, and three were broken. One by the&lt;br&gt;
empty upsert above: a bug nothing caught for &lt;strong&gt;eight releases&lt;/strong&gt;, because the&lt;br&gt;
only code exercising that path sat where no CI was looking. They run against&lt;br&gt;
the working tree now, on every push and pull request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The argument
&lt;/h2&gt;

&lt;p&gt;Every refusal here has the same shape. forge is handed two possibilities that&lt;br&gt;
look identical from where it stands, and picks neither.&lt;/p&gt;

&lt;p&gt;A rename and a drop-plus-add. A widening and a narrowing. An empty update&lt;br&gt;
clause and one that meant to change something. In each case a differ could have&lt;br&gt;
guessed, been right most of the time, and been catastrophically wrong the rest&lt;br&gt;
— and the wrongness would not surface as an error. It surfaces as an empty&lt;br&gt;
column, a foreign key pointing nowhere, an upsert returning &lt;code&gt;undefined&lt;/code&gt;, a&lt;br&gt;
migration nothing ever applied.&lt;/p&gt;

&lt;p&gt;That's the real measure of a data tool. Not the query API — everyone's is fine&lt;br&gt;
now, they converged on roughly the same shape and it took me a while to admit&lt;br&gt;
it. The measure is what a tool does at the boundary of its own knowledge.&lt;/p&gt;

&lt;p&gt;An error costs you a minute. A silent wrong answer costs you a quarter, and you&lt;br&gt;
don't find out which quarter until the accounting doesn't add up.&lt;/p&gt;

&lt;p&gt;Refusing to guess is a feature. It's the only one in this post that matters&lt;br&gt;
solely when everything else has already gone wrong — which is exactly when&lt;br&gt;
you'll care about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The series
&lt;/h2&gt;

&lt;p&gt;Five posts, and I'm grateful to everyone who read past the first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 1&lt;/strong&gt; — &lt;em&gt;"I picked MongoDB on day one. By month eight I needed Postgres."&lt;/em&gt;&lt;br&gt;
The day-one database decision, and one API over six databases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2&lt;/strong&gt; — &lt;em&gt;"The afternoon my teammate lost to a stale generated client."&lt;/em&gt;&lt;br&gt;
Inferred types instead of codegen, and &lt;code&gt;$explain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3&lt;/strong&gt; — the browser: the same &lt;code&gt;findMany&lt;/code&gt; in a tab, against SQLite compiled&lt;br&gt;
to WebAssembly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4&lt;/strong&gt; — React Native, where every native module is a rebuild.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5&lt;/strong&gt; — this one. The work that keeps a schema honest.&lt;/p&gt;

&lt;p&gt;📖 Docs: &lt;strong&gt;&lt;a href="https://johnsonfash.github.io/forge-orm/" rel="noopener noreferrer"&gt;johnsonfash.github.io/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
📦 npm: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
⭐ GitHub: &lt;strong&gt;&lt;a href="https://github.com/johnsonfash/forge-orm" rel="noopener noreferrer"&gt;johnsonfash/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Thank you for reading the whole thing — genuinely. One last question, and it's&lt;br&gt;
the one I most want answered: what's the worst thing a migration has ever done&lt;br&gt;
to your data, and did you find out from an error or from a customer?&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>database</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>The same models, on a phone with no signal</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:39:56 +0000</pubDate>
      <link>https://dev.to/johnsonfash/the-same-models-on-a-phone-with-no-signal-5a1e</link>
      <guid>https://dev.to/johnsonfash/the-same-models-on-a-phone-with-no-signal-5a1e</guid>
      <description>&lt;p&gt;There is a stretch of the Lagos–Ibadan expressway where the network drops&lt;br&gt;
for about four minutes. Not enough to notice if you're a passenger. Plenty&lt;br&gt;
if you're a Dallio user standing at the back of a customer's shop trying to&lt;br&gt;
record twelve cartons of soap against an invoice.&lt;/p&gt;

&lt;p&gt;One of the first people to use Dallio's field app was a distributor's rep&lt;br&gt;
who spent his day exactly like that: shop to shop, six or seven stops,&lt;br&gt;
somewhere between two bars of signal and none. The first version of that&lt;br&gt;
app did the obvious thing — every action was a request. Tap "record&lt;br&gt;
delivery", spinner, timeout, retry, and a rep who had learned to take a&lt;br&gt;
photo of his paper ledger as a backup. Which tells you the app had already&lt;br&gt;
lost.&lt;/p&gt;

&lt;p&gt;So the local database stopped being a cache and became the database. Write&lt;br&gt;
to SQLite on the phone, always, whether or not there's signal. Push to the&lt;br&gt;
server when there is. That part is not a novel idea; it's the standard&lt;br&gt;
shape of an offline-first app and half the mobile ecosystem is built around&lt;br&gt;
it.&lt;/p&gt;

&lt;p&gt;The problem was what it did to my models.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three codebases describing the same invoice
&lt;/h2&gt;

&lt;p&gt;By that point Dallio had a server — Node, talking to Postgres and Mongo. It&lt;br&gt;
had a browser build where the whole thing ran locally in a tab (that was post 3 —&lt;br&gt;
sqlite-wasm and IndexedDB). And now it had a phone.&lt;/p&gt;

&lt;p&gt;Same invoice in all three. Three definitions of it.&lt;/p&gt;

&lt;p&gt;The server had the real one. The web build had a copy, close enough that&lt;br&gt;
you'd never spot the difference reading them side by side, subtly wrong in&lt;br&gt;
one place — the discount field was nullable on the server and defaulted to&lt;br&gt;
zero in the browser, which meant one report disagreed with another by a few&lt;br&gt;
hundred naira and took me a full evening to find. The phone had a third,&lt;br&gt;
hand-written &lt;code&gt;CREATE TABLE&lt;/code&gt; in a string literal, because that's what you do&lt;br&gt;
when your ORM won't run on React Native.&lt;/p&gt;

&lt;p&gt;Three files. One concept. Every schema change was a three-way merge I&lt;br&gt;
performed in my head.&lt;/p&gt;
&lt;h2&gt;
  
  
  The models are the same file
&lt;/h2&gt;

&lt;p&gt;Here is what I actually wanted, and what forge does now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// models.ts — shared. Node imports it. The browser imports it. The phone imports it.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;relate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;invoices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;many&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invoices&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;relate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;customer_id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cascade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Invoice&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three ways to open it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.ts — Node, Postgres&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm/postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./models&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// web.ts — browser, sqlite-wasm over OPFS&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wasmSqliteDriver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./models&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm/wasm/worker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;wasmSqliteDriver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opfs-sahpool:///app.sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// mobile.ts — Expo&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;SQLite&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo-sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expoSqliteDriver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./models&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;expoSqliteDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SQLite&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openDatabaseSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bare React Native, where you can prebuild, uses op-sqlite instead — it talks&lt;br&gt;
JSI directly rather than crossing an async bridge per row, which shows up on&lt;br&gt;
write-heavy work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;openSqlite&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@op-engineering/op-sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opSqliteDriver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./models&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;native&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;openSqlite&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;opSqliteDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;native&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db.invoice.findMany({ where: { status: 'draft' }, include: { customer: true } })&lt;/code&gt;&lt;br&gt;
is the same call in all four files. Not a similar call. The same one, against&lt;br&gt;
the same models, fully typed, with nothing generated.&lt;/p&gt;
&lt;h2&gt;
  
  
  A warning: never import &lt;code&gt;forge-orm/sqlite&lt;/code&gt; on React Native
&lt;/h2&gt;

&lt;p&gt;This one deserves a box, because I watched someone hit it and lose an hour.&lt;/p&gt;

&lt;p&gt;forge has per-dialect entry points — &lt;code&gt;forge-orm/postgres&lt;/code&gt;, &lt;code&gt;forge-orm/mysql&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;forge-orm/sqlite&lt;/code&gt; and so on. They exist so a bundler can &lt;em&gt;see&lt;/em&gt; the driver:&lt;br&gt;
inside &lt;code&gt;forge-orm/postgres&lt;/code&gt;, &lt;code&gt;pg&lt;/code&gt; is brought in with a static import, which&lt;br&gt;
webpack and esbuild understand in a way they never understand a computed&lt;br&gt;
&lt;code&gt;require&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;forge-orm/sqlite&lt;/code&gt; is the &lt;strong&gt;better-sqlite3&lt;/strong&gt; one. better-sqlite3 is a native&lt;br&gt;
Node addon. It cannot exist on React Native — not "is slow there", not&lt;br&gt;
"needs configuration": there is no build of it that runs. Import it from an&lt;br&gt;
Expo or RN app and Metro fails on a module that has no business being in the&lt;br&gt;
graph.&lt;/p&gt;

&lt;p&gt;The entry point isn't wrong. It's a server entry point that happens to&lt;br&gt;
share a dialect name with the thing you want. On a phone you use the main&lt;br&gt;
&lt;code&gt;forge-orm&lt;/code&gt; entry and pass &lt;code&gt;driver:&lt;/code&gt; — and because &lt;em&gt;you&lt;/em&gt; write the&lt;br&gt;
&lt;code&gt;import * as SQLite from 'expo-sqlite'&lt;/code&gt; line yourself, it's static, and Metro&lt;br&gt;
resolves it like any other import. The pluggable driver is the bundler-safe&lt;br&gt;
path here, not the escape hatch.&lt;/p&gt;
&lt;h2&gt;
  
  
  One dialect, several packages
&lt;/h2&gt;

&lt;p&gt;This is the part I find genuinely interesting, and it took me a while to see&lt;br&gt;
it clearly.&lt;/p&gt;

&lt;p&gt;Fifteen driver packages sit behind forge's six dialects. SQLite alone&lt;br&gt;
accounts for six of them: &lt;code&gt;better-sqlite3&lt;/code&gt; on a server, &lt;code&gt;expo-sqlite&lt;/code&gt; and&lt;br&gt;
OP-SQLite on React Native, &lt;code&gt;@libsql/client&lt;/code&gt; for libSQL and Turso,&lt;br&gt;
&lt;code&gt;@sqlite.org/sqlite-wasm&lt;/code&gt; in a browser tab, &lt;code&gt;@tauri-apps/plugin-sql&lt;/code&gt; on&lt;br&gt;
desktop. Postgres has three, MySQL three, and MongoDB, DuckDB and SQL Server&lt;br&gt;
one each.&lt;/p&gt;

&lt;p&gt;Six packages, one adapter. Every one of those SQLite clients runs the same&lt;br&gt;
forge sqlite adapter — same IR compiler, same DDL emitter, same&lt;br&gt;
introspection reader — and emits identical SQL. The adapter has no idea&lt;br&gt;
which of them is underneath. It hands down a SQL string and an array of&lt;br&gt;
params and gets rows back; that is the entire contract, five methods wide.&lt;/p&gt;

&lt;p&gt;Which is why no two of them can share an entry point. Each would drag in a&lt;br&gt;
package the others cannot load. A single &lt;code&gt;forge-orm/sqlite&lt;/code&gt; importing all&lt;br&gt;
six would put a WebAssembly SQLite and a native Node addon into your Metro&lt;br&gt;
bundle together, and neither belongs there.&lt;/p&gt;

&lt;p&gt;Adapters are stable. Engines aren't. Keeping them apart is the reason the&lt;br&gt;
phone got the real models instead of a hand-written &lt;code&gt;CREATE TABLE&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;$migrate()&lt;/code&gt;, because there's no CLI on a phone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;forge push&lt;/code&gt; is a Node CLI. You cannot shell out to Node from an iPhone, so&lt;br&gt;
forge ships the same emitter and applier as a runtime call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// { applied, skipped, failures, alteredColumns, pending }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the schema you passed to &lt;code&gt;createDb&lt;/code&gt;, emits the same DDL &lt;code&gt;forge push&lt;/code&gt;&lt;br&gt;
would, skips tables and indexes that already exist, then introspects the live&lt;br&gt;
database and adds any missing column that can be added safely — nullable, or&lt;br&gt;
with a constant default. Destructive drift (column drops, type changes) is&lt;br&gt;
left alone and reported under &lt;code&gt;pending&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It's idempotent, so calling it on every app boot is the intended usage. On&lt;br&gt;
mobile that matters more than it does on a server, because over-the-air&lt;br&gt;
updates ship a JS bundle whose schema can run ahead of what the installed&lt;br&gt;
build's database knows about. &lt;code&gt;db.$diff()&lt;/code&gt; gives you the report without the&lt;br&gt;
apply, which makes a decent release-branch test.&lt;/p&gt;

&lt;p&gt;One Metro-specific wrinkle: fast refresh re-runs your module, so memoise the&lt;br&gt;
handle at module scope rather than opening a connection on every save.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expo managed gets stock SQLite.&lt;/strong&gt; FTS5 is there from SDK 51, but there's
no SQLCipher and no extensions — you prebuild and move to op-sqlite for
those. The forge schema, models and queries don't change when you do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geo needs fallback mode.&lt;/strong&gt; No mobile build ships SpatiaLite, so
&lt;code&gt;f.geoPoint({ fallback: true })&lt;/code&gt; stores &lt;code&gt;{lng, lat}&lt;/code&gt; as JSON and the
adapter post-filters with Haversine. Correct, and slower —
fine to roughly 50k rows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;f.vector(N)&lt;/code&gt; needs a custom build&lt;/strong&gt; that links sqlite-vec. Stock
op-sqlite and expo-sqlite don't have it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size.&lt;/strong&gt; forge itself is around 80 KB minified with no driver. The
drivers carry the weight: expo-sqlite is roughly 150 KB of JS plus about
1.2 MB of native per arch, op-sqlite roughly 600 KB plus 3–4 MB per arch.
ABI splits keep the per-APK number down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync is yours.&lt;/strong&gt; forge is local-first and ships none. MOBILE.md
documents three patterns — pull-on-foreground, an outbox table replayed
with backoff, CRDT blobs — and the outbox is a plain forge model, not a
special API. Which is the point: the local SQLite &lt;em&gt;is&lt;/em&gt; the app's database,
and sync is a separate program that happens to read and write it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rep on the expressway doesn't know any of this. He taps, it saves, the&lt;br&gt;
number is right when he gets back into signal. That was the whole&lt;br&gt;
requirement.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next in this series:&lt;/strong&gt; the CLI — &lt;code&gt;forge push&lt;/code&gt;, &lt;code&gt;forge diff&lt;/code&gt;, migration&lt;br&gt;
snapshots, and the bugs that shipped before I got drift detection right.&lt;/p&gt;

&lt;p&gt;📖 Docs: &lt;strong&gt;&lt;a href="https://johnsonfash.github.io/forge-orm/" rel="noopener noreferrer"&gt;johnsonfash.github.io/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
📦 npm: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
⭐ GitHub: &lt;strong&gt;&lt;a href="https://github.com/johnsonfash/forge-orm" rel="noopener noreferrer"&gt;johnsonfash/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've shipped an offline-first mobile app, how did you keep the phone's&lt;br&gt;
schema and the server's schema from drifting apart? I've yet to hear two&lt;br&gt;
people answer that the same way.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>typescript</category>
      <category>database</category>
    </item>
    <item>
      <title>The network died mid-sale. The customer still had cash in her hand.</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:38:37 +0000</pubDate>
      <link>https://dev.to/johnsonfash/the-network-died-mid-sale-the-customer-still-had-cash-in-her-hand-3pf2</link>
      <guid>https://dev.to/johnsonfash/the-network-died-mid-sale-the-customer-still-had-cash-in-her-hand-3pf2</guid>
      <description>&lt;p&gt;There is a shop on the ground floor of a plaza off Lagos Island. Fabric,&lt;br&gt;
mostly. Two staff. One counter. The internet is a phone on top of the till,&lt;br&gt;
tethered, propped against a bottle of water because that's the corner of the&lt;br&gt;
room where it holds a signal.&lt;/p&gt;

&lt;p&gt;I was standing in that shop when the connection dropped.&lt;/p&gt;

&lt;p&gt;The cashier had scanned three items. She tapped &lt;strong&gt;Save&lt;/strong&gt;. The button went&lt;br&gt;
into that state buttons go into — greyed, a small spinner, the polite lie&lt;br&gt;
that something is happening. The customer was holding four thousand naira,&lt;br&gt;
counted out, waiting. Ten seconds. Twenty. Behind her, two more people.&lt;/p&gt;

&lt;p&gt;The cashier did what anyone would do. She wrote it in a notebook, took the&lt;br&gt;
cash, and told me she'd "enter it later." Which she did — sometimes.&lt;/p&gt;

&lt;p&gt;That's the bug. Not the spinner. The notebook.&lt;/p&gt;

&lt;p&gt;Every sale that lands in a notebook is a sale that will not match the&lt;br&gt;
inventory count on Friday. Dallio's whole promise to a retailer is &lt;em&gt;your&lt;br&gt;
numbers are right&lt;/em&gt;, and a dropped connection was quietly turning that into&lt;br&gt;
&lt;em&gt;your numbers are right when NTEL is having a good day&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What you normally do about this
&lt;/h2&gt;

&lt;p&gt;You cache. Of course you cache.&lt;/p&gt;

&lt;p&gt;The first version is &lt;code&gt;localStorage&lt;/code&gt; and a JSON blob. It works for about a&lt;br&gt;
week, until you need to find "unsynced sales for this branch since Monday"&lt;br&gt;
and discover you're writing &lt;code&gt;Array.prototype.filter&lt;/code&gt; over a parsed string.&lt;/p&gt;

&lt;p&gt;The second version is a hand-rolled IndexedDB wrapper. Now you have stores,&lt;br&gt;
and keys, and a promise shim, and you've reinvented about 15% of a query&lt;br&gt;
engine, badly, in a file nobody wants to open.&lt;/p&gt;

&lt;p&gt;The third version is the honest one: a second data layer. Local models that&lt;br&gt;
shadow your server models. A second set of types. A second implementation of&lt;br&gt;
soft delete, of tenant scoping, of "don't show voided sales." Every bug fixed&lt;br&gt;
on the server has to be fixed again, slightly differently, in the tab.&lt;/p&gt;

&lt;p&gt;I'd already lived that in post 1 — Mongo for operations, Postgres for&lt;br&gt;
reporting, two of everything. I was not doing it a third time in JavaScript.&lt;/p&gt;
&lt;h2&gt;
  
  
  The version I wanted
&lt;/h2&gt;

&lt;p&gt;The same models. The same &lt;code&gt;findMany&lt;/code&gt;. A different driver.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sales&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;startOfDay&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That line should not know or care whether it's talking to Postgres over TCP&lt;br&gt;
or to a database living inside the tab. forge ships no driver of its own —&lt;br&gt;
every backend is an optional peer dependency, which sounded like packaging&lt;br&gt;
trivia when I wrote post 1 and turned out to be the reason any of this is&lt;br&gt;
possible.&lt;/p&gt;

&lt;p&gt;There are two browser tiers. They read the same schema and take the same&lt;br&gt;
calls.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tier one: IndexedDB, nothing to install
&lt;/h2&gt;

&lt;p&gt;Every browser since roughly 2017 has IndexedDB built in. No wasm blob to&lt;br&gt;
download and host, no Web Worker, no bundler plugin, no COOP/COEP headers to&lt;br&gt;
argue with your CDN about.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Side-effect import registers the IndexedDB adapter with the factory.&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm/indexeddb&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sales&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uuid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Sale&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idb:dallio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;idb:&lt;/code&gt; prefix (or the alias &lt;code&gt;indexeddb:&lt;/code&gt;) picks the adapter, and the&lt;br&gt;
string after the colon is the IndexedDB database name — the same thing you'd&lt;br&gt;
hand to &lt;code&gt;indexedDB.open(name)&lt;/code&gt; yourself. If you'd rather not thread a URL,&lt;br&gt;
&lt;code&gt;indexedDbDriver({ name: 'dallio' })&lt;/code&gt; from &lt;code&gt;forge-orm/indexeddb&lt;/code&gt; gets you the&lt;br&gt;
same adapter and lets you hand in a database you opened in your own bootstrap&lt;br&gt;
code.&lt;/p&gt;

&lt;p&gt;That's the whole setup. Installed size of the database layer: zero bytes over&lt;br&gt;
what you already shipped.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;$migrate()&lt;/code&gt;, because there is no CLI in a browser tab
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;forge push&lt;/code&gt; is a Node CLI. You cannot shell out to Node from a tab, so the&lt;br&gt;
same DDL emitter is exposed as a runtime call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it at app boot. It's idempotent — existing stores and indexes are&lt;br&gt;
skipped, and on the SQLite tier, columns added since the last boot get&lt;br&gt;
patched in with &lt;code&gt;ALTER TABLE … ADD COLUMN&lt;/code&gt; where that's safe. Anything&lt;br&gt;
destructive (a dropped column, a type change) is left alone and reported&lt;br&gt;
under &lt;code&gt;report.pending&lt;/code&gt; for you to decide about, rather than silently eating&lt;br&gt;
someone's sales history.&lt;/p&gt;

&lt;p&gt;On IndexedDB it maps onto the platform's own &lt;code&gt;onupgradeneeded&lt;/code&gt; versioning,&lt;br&gt;
and rather well: adding a field is a no-op because IDB is schemaless, and&lt;br&gt;
adding an index back-populates existing rows for free. The engine&lt;br&gt;
fingerprints the DDL plan and only bumps the IDB version when the fingerprint&lt;br&gt;
changes, so &lt;code&gt;$migrate()&lt;/code&gt; on an unchanged schema is a boot-time metadata check&lt;br&gt;
and nothing more.&lt;/p&gt;
&lt;h2&gt;
  
  
  The offline-first shape
&lt;/h2&gt;

&lt;p&gt;Local write first, sync second. Never the other way round.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;saleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outbox is an ordinary model in the same schema — id, target id, op, a&lt;br&gt;
&lt;code&gt;f.json()&lt;/code&gt; payload, &lt;code&gt;queuedAt&lt;/code&gt;, &lt;code&gt;tries&lt;/code&gt;. A drain loop empties it when the&lt;br&gt;
connection comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queuedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;op&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pushToServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;op&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cashier's &lt;strong&gt;Save&lt;/strong&gt; button now returns in a millisecond and is honest&lt;br&gt;
about it. The queue depth goes in the corner of the screen — &lt;em&gt;3 pending&lt;/em&gt; —&lt;br&gt;
because a shop owner would rather see a number than trust a spinner.&lt;/p&gt;

&lt;p&gt;That pattern is &lt;code&gt;examples/02-sqlite-browser-offline-first&lt;/code&gt;, and it is the&lt;br&gt;
shape Dallio's POS uses.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tier two: real SQLite, over OPFS
&lt;/h2&gt;

&lt;p&gt;IndexedDB stops being enough at a fairly specific moment: when you want the&lt;br&gt;
browser to answer a &lt;em&gt;question&lt;/em&gt; rather than hand you rows.&lt;/p&gt;

&lt;p&gt;The end-of-day report joins sales to line items to products, groups by&lt;br&gt;
category, and sums. On the SQLite tier that's SQL — the same C engine that&lt;br&gt;
runs on a server, compiled to WebAssembly, in a Web Worker, persisted on the&lt;br&gt;
Origin Private File System.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wasmSqliteDriver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm/wasm/worker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;wasmSqliteDriver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opfs-sahpool:///dallio.sqlite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three URL schemes: &lt;code&gt;opfs-sahpool:///file.sqlite&lt;/code&gt; is the default and the one&lt;br&gt;
to pick — its VFS coordinates access handles across tabs, so a user with your&lt;br&gt;
app open twice doesn't get a surprise. &lt;code&gt;opfs:///file.sqlite&lt;/code&gt; is slightly&lt;br&gt;
faster and single-writer; a second tab opening the same file throws&lt;br&gt;
&lt;code&gt;SQLITE_BUSY&lt;/code&gt;. &lt;code&gt;:memory:&lt;/code&gt; is for tests and demos, and dies with the tab.&lt;/p&gt;

&lt;p&gt;The worker is required, not stylistic — OPFS synchronous access handles only&lt;br&gt;
exist inside a Worker. Bundler wiring is one import: &lt;code&gt;forgeWasm()&lt;/code&gt; from&lt;br&gt;
&lt;code&gt;forge-orm/wasm/vite&lt;/code&gt;, &lt;code&gt;withForgeWasm()&lt;/code&gt; from &lt;code&gt;forge-orm/wasm/next&lt;/code&gt;, or&lt;br&gt;
&lt;code&gt;forgeWasmWebpack(config)&lt;/code&gt; from &lt;code&gt;forge-orm/wasm/webpack&lt;/code&gt;. Parcel needs&lt;br&gt;
nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pick sqlite-wasm when&lt;/strong&gt; you need real joins, &lt;code&gt;groupBy&lt;/code&gt;/&lt;code&gt;having&lt;/code&gt;, FTS5 with&lt;br&gt;
BM25 ranking, or more data than a cursor scan enjoys. &lt;strong&gt;IndexedDB is enough&lt;br&gt;
when&lt;/strong&gt; your browser workload is a working set — today's sales, this branch's&lt;br&gt;
stock list, an outbox — that you filter and sort and page, and the serious&lt;br&gt;
reporting happens on a server.&lt;/p&gt;

&lt;p&gt;Cost of the second tier: about 1 MB of wasm, a worker file, and COOP/COEP&lt;br&gt;
headers to get right. Code-split it so your landing page doesn't pay.&lt;/p&gt;
&lt;h2&gt;
  
  
  The limits, stated plainly
&lt;/h2&gt;

&lt;p&gt;IndexedDB has no SQL. forge compiles your query to an IR, the planner picks&lt;br&gt;
&lt;strong&gt;one&lt;/strong&gt; index, scans a cursor over it, and applies the rest as a compiled JS&lt;br&gt;
predicate. That's honest and it's fast enough at the sizes browsers actually&lt;br&gt;
hold — the docs put the comfortable ceiling around 100k rows — but it is not&lt;br&gt;
a query optimiser.&lt;/p&gt;

&lt;p&gt;Specifics worth knowing before you commit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AND&lt;/code&gt; at the root of a &lt;code&gt;where&lt;/code&gt; unlocks index selection. &lt;code&gt;OR&lt;/code&gt; and &lt;code&gt;NOT&lt;/code&gt; at
the root fall back to a full-table scan plus the residual predicate.&lt;/li&gt;
&lt;li&gt;Relations work, but there is no join. The executor does the lookups.&lt;/li&gt;
&lt;li&gt;JSON path queries (&lt;code&gt;{ meta: { path: 'address.city', equals: 'Lagos' } }&lt;/code&gt;)
get no index acceleration at all. If a path goes hot, promote it to a real
field with its own index.&lt;/li&gt;
&lt;li&gt;Vector search is brute-force cosine/L2 in JS — fine to roughly 1k vectors,
slow past that. Geo is a bbox prefilter plus Haversine. On stock sqlite-wasm
those two are &lt;em&gt;also&lt;/em&gt; fallbacks; native R-Tree and sqlite-vec need a custom
wasm build.&lt;/li&gt;
&lt;li&gt;Full-text is a multiEntry token index and an AND of tokens. No stemming, no
stopwords, no ranking.&lt;/li&gt;
&lt;li&gt;IDB transactions auto-commit the moment the microtask queue idles. That is
a platform rule, not a forge choice. &lt;code&gt;$transaction(fn)&lt;/code&gt; gives you
best-effort atomicity and rollback on throw, but not strict
serialisability — a &lt;code&gt;fetch&lt;/code&gt; between two writes will let the first commit
alone. For genuine atomicity use the array form, &lt;code&gt;$transaction([a, b])&lt;/code&gt;,
which maps to one &lt;code&gt;readwrite&lt;/code&gt; transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the one that catches everybody: &lt;strong&gt;Safari deletes it&lt;/strong&gt;. Since 2020, ITP&lt;br&gt;
clears IndexedDB, OPFS and LocalStorage for any site the user hasn't visited&lt;br&gt;
in 7 days. Same rule for both tiers. The mitigation is the same for both too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persistence is granted automatically to installed PWAs and after enough&lt;br&gt;
engagement elsewhere; the first call often returns &lt;code&gt;false&lt;/code&gt;, so retry on later&lt;br&gt;
visits. For data the user can't get back, tell them to add the app to their&lt;br&gt;
home screen and mean it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it fix the shop?
&lt;/h2&gt;

&lt;p&gt;The spinner is gone. Sales write to the tab in a millisecond, the outbox&lt;br&gt;
drains when the phone finds a signal, and the notebook has stopped being part&lt;br&gt;
of the accounting system.&lt;/p&gt;

&lt;p&gt;The part I keep being pleased about is what &lt;em&gt;didn't&lt;/em&gt; happen: no second set of&lt;br&gt;
models, no shadow query layer, no local-only bugs. &lt;code&gt;db.sale.findMany&lt;/code&gt; in the&lt;br&gt;
browser is the same call, against the same model, as &lt;code&gt;db.sale.findMany&lt;/code&gt; on&lt;br&gt;
the server.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next in this series:&lt;/strong&gt; the same models on a phone. React Native and Expo,&lt;br&gt;
where there is no OPFS, no IndexedDB, and a different driver again.&lt;/p&gt;

&lt;p&gt;📖 Docs: &lt;strong&gt;&lt;a href="https://johnsonfash.github.io/forge-orm/" rel="noopener noreferrer"&gt;johnsonfash.github.io/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
📦 npm: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
⭐ GitHub: &lt;strong&gt;&lt;a href="https://github.com/johnsonfash/forge-orm" rel="noopener noreferrer"&gt;johnsonfash/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's the worst connection your app has ever had to survive? I'd like to&lt;br&gt;
know whether Lagos is unusual or whether everyone's been quietly building&lt;br&gt;
notebooks.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>database</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The afternoon my teammate lost to a stale generated client</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:37:45 +0000</pubDate>
      <link>https://dev.to/johnsonfash/the-afternoon-my-teammate-lost-to-a-stale-generated-client-26am</link>
      <guid>https://dev.to/johnsonfash/the-afternoon-my-teammate-lost-to-a-stale-generated-client-26am</guid>
      <description>&lt;p&gt;Someone on my team once spent most of a Thursday convinced I had broken the&lt;br&gt;
build.&lt;/p&gt;

&lt;p&gt;He'd pulled a branch where I'd renamed a column. Nothing exotic: &lt;code&gt;phone&lt;/code&gt;&lt;br&gt;
became &lt;code&gt;phoneNumber&lt;/code&gt;, one line in the schema file, one migration. He pulled,&lt;br&gt;
ran the app, and started wiring up the settings form. Autocomplete offered him&lt;br&gt;
&lt;code&gt;phone&lt;/code&gt;. He used &lt;code&gt;phone&lt;/code&gt;. TypeScript was happy. The editor was happy. At&lt;br&gt;
runtime the field came back &lt;code&gt;undefined&lt;/code&gt;, every time, on a column that was&lt;br&gt;
sitting right there in the database with data in it.&lt;/p&gt;

&lt;p&gt;He checked the migration. He checked the connection string. He checked whether&lt;br&gt;
he was pointed at the wrong branch's database. Around five o'clock he asked me&lt;br&gt;
to look, and I said the sentence you say roughly once a fortnight on a Prisma&lt;br&gt;
codebase: &lt;em&gt;did you run &lt;code&gt;prisma generate&lt;/code&gt;?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;His generated client was three commits old. It had been describing a database&lt;br&gt;
that no longer existed, confidently, in green squiggle-free TypeScript, for&lt;br&gt;
about four hours.&lt;/p&gt;

&lt;p&gt;That is not a Prisma bug. It is the shape of the thing.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the codegen step exists
&lt;/h2&gt;

&lt;p&gt;I want to be fair here, because &lt;code&gt;prisma generate&lt;/code&gt; is not a mistake and it is&lt;br&gt;
not laziness. It solves a real problem.&lt;/p&gt;

&lt;p&gt;Prisma's schema lives in &lt;code&gt;schema.prisma&lt;/code&gt;, its own DSL file. TypeScript cannot&lt;br&gt;
read that file. The compiler has no idea it exists. So something has to stand&lt;br&gt;
between the two and turn one into the other, and that something is a&lt;br&gt;
generator that writes a concrete &lt;code&gt;.d.ts&lt;/code&gt; describing your models. Once written,&lt;br&gt;
those types are ordinary declared types. The compiler reads them the way it&lt;br&gt;
reads any other declaration file, which is fast, predictable, and scales to&lt;br&gt;
schemas with three hundred models without your editor going quiet for four&lt;br&gt;
seconds every time you type a dot.&lt;/p&gt;

&lt;p&gt;There's a second thing codegen buys. Because the client is generated per&lt;br&gt;
project, Prisma can bake in exactly the operations your schema supports and&lt;br&gt;
nothing else, and it can ship a query engine binary alongside it. That engine&lt;br&gt;
is where a lot of Prisma's correctness and speed lives.&lt;/p&gt;

&lt;p&gt;Both of those are genuine wins. I don't think the people who built it were&lt;br&gt;
wrong.&lt;/p&gt;

&lt;p&gt;I do think the bill is higher than it looks.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bill
&lt;/h2&gt;

&lt;p&gt;You regenerate after every schema change, and after every pull that contains&lt;br&gt;
someone else's schema change, and after &lt;code&gt;npm ci&lt;/code&gt; wipes &lt;code&gt;node_modules&lt;/code&gt;. Miss one&lt;br&gt;
and you don't get an error, you get four hours of a colleague's Thursday.&lt;/p&gt;

&lt;p&gt;You then choose between two unattractive options for source control. Gitignore&lt;br&gt;
the generated client, and every fresh checkout is broken until a postinstall&lt;br&gt;
hook fixes it. Commit it, and every schema change lands as a pull request with&lt;br&gt;
one meaningful line and forty thousand generated ones, which nobody reviews,&lt;br&gt;
which is exactly where the merge conflicts live.&lt;/p&gt;

&lt;p&gt;Then there's the engine binary. It's platform-specific. I've had a deploy fail&lt;br&gt;
because the image built on one libc and ran on another, and the error you get&lt;br&gt;
at the far end of that is not "wrong binary target", it's a stack trace from&lt;br&gt;
inside a Rust process at container start. It's fixable. It's documented. It's&lt;br&gt;
still twenty minutes of your life at a bad moment.&lt;/p&gt;

&lt;p&gt;And the part that ended it for me: none of this follows you into a browser tab&lt;br&gt;
or a React Native bundle. A native binary and a generation step don't cross&lt;br&gt;
that boundary. I'll come back to why I cared so much about that.&lt;/p&gt;
&lt;h2&gt;
  
  
  The other route
&lt;/h2&gt;

&lt;p&gt;The alternative is not clever. It's the one TypeScript has been offering all&lt;br&gt;
along: don't describe your schema in a foreign language, describe it in&lt;br&gt;
TypeScript, and let inference do the work.&lt;/p&gt;

&lt;p&gt;In forge, &lt;code&gt;f.string()&lt;/code&gt; builds a plain object. So does &lt;code&gt;f.int().optional()&lt;/code&gt;.&lt;br&gt;
&lt;code&gt;model()&lt;/code&gt; collects them into an object and hands back a &lt;code&gt;TypedModel&lt;/code&gt; carrying&lt;br&gt;
the field map as a phantom type property, one that exists for the compiler and&lt;br&gt;
not at runtime. &lt;code&gt;createDb&lt;/code&gt; captures that map through a generic. Every call site&lt;br&gt;
resolves against it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dateTime&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;now&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;//    ^ { id: string; email: string; name: string | null; age: number | null; createdAt: Date }[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rename &lt;code&gt;age&lt;/code&gt; to &lt;code&gt;ageYears&lt;/code&gt; in that model and the &lt;code&gt;where&lt;/code&gt; clause goes red&lt;br&gt;
before you've finished typing the &lt;code&gt;s&lt;/code&gt;. Not on the next build. Not after a&lt;br&gt;
command you have to remember. There is no generated artefact that can be older&lt;br&gt;
than your schema, because there is no generated artefact.&lt;/p&gt;

&lt;p&gt;The shapes are exported as type helpers, so you can use them at service&lt;br&gt;
boundaries rather than hand-writing DTOs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Infer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;InferCreate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;InferWhere&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;InferSchema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserRow&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;//   { id: string; email: string; name: string | null; age: number | null; createdAt: Date }&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserCreate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;InferCreate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;//   every scalar optional — defaults are filled at runtime&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserWhere&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;InferWhere&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Or the whole bundle for one model:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Infer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;UserT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Row&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;UserT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Create&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;UserT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Update&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;UserT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Where&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="nx"&gt;UserT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Include&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Or every model at once — the right shape to export from a shared types.ts:&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;InferSchema&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserSelect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;InferRow&lt;/code&gt;, &lt;code&gt;InferUpdate&lt;/code&gt;, &lt;code&gt;InferUpsert&lt;/code&gt;, &lt;code&gt;InferOrderBy&lt;/code&gt;, &lt;code&gt;InferSelect&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;InferInclude&lt;/code&gt; and &lt;code&gt;InferOmit&lt;/code&gt; are all there too. None of them require you to&lt;br&gt;
register anything or call a build command. One gotcha worth knowing: the schema&lt;br&gt;
map needs &lt;code&gt;as const&lt;/code&gt;, or &lt;code&gt;keyof typeof schema&lt;/code&gt; widens to &lt;code&gt;string&lt;/code&gt; and the&lt;br&gt;
relation walk quietly falls back to a loose shape.&lt;/p&gt;
&lt;h2&gt;
  
  
  What this costs, honestly
&lt;/h2&gt;

&lt;p&gt;Inference is not free, and I'd rather say so than have you find out on a large&lt;br&gt;
schema.&lt;/p&gt;

&lt;p&gt;Prisma's generated types are concrete. The compiler reads them once and moves&lt;br&gt;
on. forge's are computed: &lt;code&gt;where&lt;/code&gt; is a mapped type walking your field map,&lt;br&gt;
the return type is a conditional type that looks at the literal type of the&lt;br&gt;
arguments you passed and resolves the row, the projection, or the relation&lt;br&gt;
tree. On a small or medium schema you will not notice. On a very large one,&lt;br&gt;
with deep &lt;code&gt;include&lt;/code&gt; chains, type-checking has more work to do than reading a&lt;br&gt;
declaration file does, and that shows up in editor responsiveness and cold&lt;br&gt;
&lt;code&gt;tsc&lt;/code&gt; runs. I have not benchmarked the crossover point and I'm not going to&lt;br&gt;
invent one, but the direction is real and it favours codegen at the top end.&lt;br&gt;
The relation walk is capped at ten levels for exactly this reason; past that&lt;br&gt;
nested args type loosely rather than exploding.&lt;/p&gt;

&lt;p&gt;There's a second gap. To support composite-unique keys and raw Mongo&lt;br&gt;
operators, &lt;code&gt;where&lt;/code&gt; carries a string index signature at the bottom, which means&lt;br&gt;
&lt;code&gt;where: { nonexistent_field: 'x' }&lt;/code&gt; compiles and then matches nothing.&lt;br&gt;
&lt;code&gt;createDb({ schema, strict: true })&lt;/code&gt; catches it at runtime with the valid keys&lt;br&gt;
listed. Turn it on in development.&lt;/p&gt;

&lt;p&gt;That's the trade. Live types with nothing to regenerate, against slower&lt;br&gt;
type-checking at the large end and one loose escape hatch.&lt;/p&gt;
&lt;h2&gt;
  
  
  "But then how do I know what SQL runs?"
&lt;/h2&gt;

&lt;p&gt;This is the fair objection to dropping the generated client, and it's the one&lt;br&gt;
I care most about answering. With Prisma you can at least go and read the&lt;br&gt;
thing. So forge has &lt;code&gt;db.$explain()&lt;/code&gt;, which takes a call site and gives you the&lt;br&gt;
statement without executing it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$explain&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findMany&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="n"&gt;users&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sqlite&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"age"&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"age"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;

  &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;

  &lt;span class="c1"&gt;-- with values inlined (for reading, not for running):&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"age"&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"users"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"age"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;q&lt;/code&gt; handed to the callback holds no session and reaches no driver, so&lt;br&gt;
nothing can execute even by accident. You get &lt;code&gt;artifact.sql&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;artifact.params&lt;/code&gt; as the pair you'd actually run, plus a &lt;code&gt;readable&lt;/code&gt; version&lt;br&gt;
with values substituted for pasting into &lt;code&gt;psql&lt;/code&gt;. Don't execute that one; the&lt;br&gt;
quoting there serves legibility, not safety.&lt;/p&gt;

&lt;p&gt;Pass &lt;code&gt;{ analyze: true }&lt;/code&gt; and it asks the database for its own plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$explain&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u7&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;  &lt;span class="c1"&gt;-- plan:&lt;/span&gt;
  &lt;span class="n"&gt;SCAN&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SCAN&lt;/code&gt; means every row read. &lt;code&gt;name&lt;/code&gt; has no index. Add one and the same query&lt;br&gt;
comes back as &lt;code&gt;SEARCH users USING INDEX users_name (name=?)&lt;/code&gt;. That's a&lt;br&gt;
property of the query rather than of the schema, which is why a schema linter&lt;br&gt;
can't see it.&lt;/p&gt;

&lt;p&gt;One deliberate omission: forge never emits &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;. &lt;code&gt;EXPLAIN&lt;/code&gt; plans&lt;br&gt;
a statement; &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; runs it. On a &lt;code&gt;SELECT&lt;/code&gt; that difference is&lt;br&gt;
accuracy. On &lt;code&gt;deleteMany&lt;/code&gt; it's your data, and an API whose entire promise is&lt;br&gt;
"this does not run" must not delete rows because you asked for more detail. If&lt;br&gt;
you want real timings, run the statement yourself through &lt;code&gt;$queryRaw&lt;/code&gt;, on&lt;br&gt;
something you meant to execute.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next in this series:&lt;/strong&gt; the reason none of this has a build step or a binary&lt;br&gt;
in the first place. The same query code, the same models, running in a browser&lt;br&gt;
tab against SQLite compiled to WebAssembly.&lt;/p&gt;

&lt;p&gt;📖 Docs: &lt;strong&gt;&lt;a href="https://johnsonfash.github.io/forge-orm/" rel="noopener noreferrer"&gt;johnsonfash.github.io/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
📦 npm: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
⭐ GitHub: &lt;strong&gt;&lt;a href="https://github.com/johnsonfash/forge-orm" rel="noopener noreferrer"&gt;johnsonfash/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What's the longest you've spent debugging something that turned out to be a&lt;br&gt;
stale generated artefact? I'd like to know I'm not alone in this.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>database</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I picked MongoDB on day one. By month eight I needed Postgres.</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Fri, 04 Sep 2026 05:36:34 +0000</pubDate>
      <link>https://dev.to/johnsonfash/i-picked-mongodb-on-day-one-by-month-eight-i-needed-postgres-3991</link>
      <guid>https://dev.to/johnsonfash/i-picked-mongodb-on-day-one-by-month-eight-i-needed-postgres-3991</guid>
      <description>&lt;p&gt;The decision took about nine minutes.&lt;/p&gt;

&lt;p&gt;It was the start of a new product — VirtualRx, a business-management app for&lt;br&gt;
small retailers in Lagos. Point of sale, inventory, invoices, the lot. I&lt;br&gt;
needed a database, I had a schema roughly in my head, and MongoDB let me&lt;br&gt;
start writing code that afternoon instead of arguing with migrations. Nine&lt;br&gt;
minutes. &lt;code&gt;createdb&lt;/code&gt;, connection string, first model, move on.&lt;/p&gt;

&lt;p&gt;That decision was fine for about eight months.&lt;/p&gt;

&lt;p&gt;Then a client asked a question I couldn't answer: &lt;em&gt;"Which of my three&lt;br&gt;
branches actually makes money on Tuesdays?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's not an exotic question. It's a &lt;code&gt;GROUP BY&lt;/code&gt; with a couple of joins and a&lt;br&gt;
window function. On Postgres it's ten lines. On Mongo it was an aggregation&lt;br&gt;
pipeline I rewrote four times, got working, and then couldn't read a week&lt;br&gt;
later. I remember staring at a &lt;code&gt;$lookup&lt;/code&gt; inside a &lt;code&gt;$facet&lt;/code&gt; and thinking:&lt;br&gt;
&lt;strong&gt;I chose this in nine minutes, and now I'm stuck with it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The obvious move was to add Postgres for reporting. Keep Mongo for the&lt;br&gt;
operational stuff, pipe the analytics somewhere that likes analytics. Sensible.&lt;br&gt;
Boring. Standard.&lt;/p&gt;

&lt;p&gt;Except that meant two data layers. Two sets of models. Two mental models. And&lt;br&gt;
every helper I'd written — soft delete, tenant scoping, audit logging — needed&lt;br&gt;
a second implementation that behaved &lt;em&gt;almost&lt;/em&gt; the same.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I actually wanted
&lt;/h2&gt;

&lt;p&gt;I wanted to write this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;locationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;postedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…and have it not care which database was underneath.&lt;/p&gt;

&lt;p&gt;Not "abstract away SQL." Not "one query language to rule them all." Just:&lt;br&gt;
&lt;strong&gt;the ordinary 90% of queries should be portable, and the other 10% should&lt;br&gt;
drop to raw SQL without ceremony.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I looked at what existed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prisma&lt;/strong&gt; has the API shape I wanted. But it's a code generation step. Every&lt;br&gt;
schema change means &lt;code&gt;prisma generate&lt;/code&gt;, a regenerated client committed or&lt;br&gt;
gitignored, a Rust query engine binary in your deploy, and a real fight to run&lt;br&gt;
in a browser or on React Native. I've watched a teammate lose an afternoon to a&lt;br&gt;
stale generated client that &lt;em&gt;looked&lt;/em&gt; fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drizzle&lt;/strong&gt; is excellent and I nearly used it. But it's SQL-shaped by design —&lt;br&gt;
which is the right call for a SQL-only tool, and the wrong one when one of my&lt;br&gt;
six targets is MongoDB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TypeORM&lt;/strong&gt; and friends: decorators, metadata, a lot of machinery.&lt;/p&gt;

&lt;p&gt;None of them ran in a browser tab. That mattered more than I expected, and I'll&lt;br&gt;
come back to it in a later post.&lt;/p&gt;

&lt;p&gt;So I did the thing you're not supposed to do. I wrote my own.&lt;/p&gt;
&lt;h2&gt;
  
  
  forge-orm
&lt;/h2&gt;

&lt;p&gt;It's a small, Prisma-shaped data layer for &lt;strong&gt;MongoDB, PostgreSQL, MySQL,&lt;br&gt;
SQLite, DuckDB and SQL Server&lt;/strong&gt;. You write models once in plain TypeScript and&lt;br&gt;
the same query code runs against any of them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;forge-orm
npm &lt;span class="nb"&gt;install &lt;/span&gt;pg          &lt;span class="c"&gt;# only the driver you actually use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// postgres:// … | mongodb:// … | sqlite: …&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;adults&lt;/code&gt; is fully typed. There was no build step. There is no generated client&lt;br&gt;
in your repo. Change the model, and the types change on the next keystroke —&lt;br&gt;
because they're inferred from the object you just wrote, not produced by a&lt;br&gt;
tool you have to remember to run.&lt;/p&gt;

&lt;p&gt;Point &lt;code&gt;DATABASE_URL&lt;/code&gt; at Mongo instead and that same &lt;code&gt;findMany&lt;/code&gt; runs as a Mongo&lt;br&gt;
&lt;code&gt;find&lt;/code&gt;. Point it at SQLite and it's a &lt;code&gt;SELECT&lt;/code&gt;. The query code doesn't move.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part I didn't expect to matter
&lt;/h2&gt;

&lt;p&gt;forge ships &lt;strong&gt;no database driver of its own&lt;/strong&gt;. Each one is an optional peer&lt;br&gt;
dependency, so &lt;code&gt;npm install forge-orm&lt;/code&gt; pulls nothing extra, and importing forge&lt;br&gt;
needs no driver at all.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;URL starts with&lt;/th&gt;
&lt;th&gt;Install&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgres://&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install pg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL / MariaDB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mysql://&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install mysql2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sqlite:&lt;/code&gt; or &lt;code&gt;file:&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install better-sqlite3&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mongodb://&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install mongodb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DuckDB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;duckdb:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install @duckdb/node-api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL Server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mssql:&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install mssql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That sounds like packaging trivia. It turned out to be the thing that let forge&lt;br&gt;
run in places an ORM normally can't — a browser tab, an Expo app, an embedded&lt;br&gt;
Postgres compiled to WebAssembly. Those posts are coming.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;I want to be straight about this, because I've been burned by READMEs that&lt;br&gt;
weren't.&lt;/p&gt;

&lt;p&gt;forge is &lt;strong&gt;not&lt;/strong&gt; a replacement for Prisma or Drizzle in maturity. It has fewer&lt;br&gt;
features, a much smaller ecosystem, and no GUI. Prisma's migration tooling is&lt;br&gt;
more battle-tested than mine. Drizzle's SQL surface is broader. If you need&lt;br&gt;
either of those things, use them — genuinely.&lt;/p&gt;

&lt;p&gt;What forge gives you is a small dependency you can read in an afternoon, one&lt;br&gt;
query API across six databases, full autocomplete with nothing to regenerate,&lt;br&gt;
and the ability to drop to raw SQL whenever the abstraction stops helping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$queryRaw&lt;/span&gt;&lt;span class="s2"&gt;`
  SELECT location_id, SUM(total) AS revenue
  FROM sales WHERE posted_at &amp;gt;= &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
  GROUP BY location_id
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That escape hatch is not an admission of failure. It's the point. An ORM that&lt;br&gt;
tries to own every query eventually owns the ones it's bad at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it actually solve the problem?
&lt;/h2&gt;

&lt;p&gt;VirtualRx still runs on MongoDB. The analytics moved to a Postgres read model.&lt;br&gt;
Both go through the same &lt;code&gt;db.&amp;lt;model&amp;gt;.findMany&lt;/code&gt; calls, the same models, the same&lt;br&gt;
soft-delete and tenant-scoping helpers. The report that started all this is&lt;br&gt;
about fifteen lines now.&lt;/p&gt;

&lt;p&gt;And the nine-minute decision stopped being permanent, which is really all I&lt;br&gt;
wanted.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Next in this series:&lt;/strong&gt; the codegen step — why &lt;code&gt;prisma generate&lt;/code&gt; exists, what&lt;br&gt;
it's actually solving, and how you get the same autocomplete without it.&lt;/p&gt;

&lt;p&gt;📖 Docs: &lt;strong&gt;&lt;a href="https://johnsonfash.github.io/forge-orm/" rel="noopener noreferrer"&gt;johnsonfash.github.io/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
📦 npm: &lt;strong&gt;&lt;a href="https://www.npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
⭐ GitHub: &lt;strong&gt;&lt;a href="https://github.com/johnsonfash/forge-orm" rel="noopener noreferrer"&gt;johnsonfash/forge-orm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you've been stuck with a day-one database decision, I'd genuinely like to&lt;br&gt;
hear what it was. That's half of why I wrote this.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>database</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Prisma can now work in your browser, expo/react-native apps? — meet forge-orm</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Mon, 06 Jul 2026 09:16:02 +0000</pubDate>
      <link>https://dev.to/johnsonfash/prisma-can-now-work-in-your-browser-exporeact-native-apps-meet-forge-orm-1khb</link>
      <guid>https://dev.to/johnsonfash/prisma-can-now-work-in-your-browser-exporeact-native-apps-meet-forge-orm-1khb</guid>
      <description>&lt;p&gt;I love Prisma. I have shipped Prisma to production more times than I care to count. It changed how a lot of us write TypeScript against databases.&lt;/p&gt;

&lt;p&gt;But here's the thing nobody tells you about Prisma:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It doesn't run in the browser.&lt;/li&gt;
&lt;li&gt;It doesn't run on Cloudflare Workers unless you buy the accelerator.&lt;/li&gt;
&lt;li&gt;It runs on iOS through a weird JSI bridge I can never remember how to configure.&lt;/li&gt;
&lt;li&gt;On Expo you eventually give up and use expo-sqlite raw.&lt;/li&gt;
&lt;li&gt;On DuckDB? MSSQL? IndexedDB? MongoDB? Enjoy either a codegen dance or "unsupported."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So awhile ago I did a very dumb thing. I opened an editor and wrote:&lt;/p&gt;

&lt;p&gt;import { createDb, f, model, tauriSqlDriver } from "forge-orm"&lt;/p&gt;

&lt;p&gt;Then I spent couple of months making that import actually work. On Postgres. On MySQL. On SQLite. On MongoDB. On DuckDB. On MSSQL. On IndexedDB. On sqlite-wasm inside a browser tab with OPFS. On Tauri. On React Native.&lt;/p&gt;

&lt;p&gt;Same easy query API on all of them. Prisma-shape. No codegen. Type-safe end to end.&lt;/p&gt;

&lt;p&gt;Let me show you what it does.&lt;/p&gt;

&lt;p&gt;Pick any environment. Here's Node.js + SQLite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sqlite:./app.db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ada@x.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ada&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@x.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, this is where it gets fun. Take that exact same code, and switch two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm/indexeddb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;                       &lt;span class="c1"&gt;// ← switch adapter&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idb:app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                               &lt;span class="c1"&gt;// ← switch URL&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else? Same. db.user.create(). db.user.findMany(). Same query shape. Same TypeScript inference. Same schema definition.&lt;/p&gt;

&lt;p&gt;It's now running in a Chrome tab, storing to IndexedDB, and you didn't write a single line of indexedDB.open().&lt;/p&gt;

&lt;p&gt;But what if I want real SQL in the browser?&lt;/p&gt;

&lt;p&gt;It got you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;wasmSqliteDriver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm/wasm/worker&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;module&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;wasmSqliteDriver&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opfs-sahpool:///app.sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// real SQLite file in OPFS&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;sqlite-wasm running in a Web Worker, writing to a persistent OPFS file, exposing the same Prisma-shape query surface. No, I did not have a fun weekend making that work.&lt;/p&gt;

&lt;p&gt;But my POS runs on Tauri!&lt;/p&gt;

&lt;p&gt;I know. My POS runs on Tauri too. That's kind of why forge-orm exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@tauri-apps/plugin-sql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tauriSqlDriver&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sqlite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sqlite:app.db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;tauriSqlDriver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sqlite&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Native SQLite through Tauri's sqlx bridge. Same query API. Wave your hand at your platform.&lt;/p&gt;

&lt;p&gt;But my Cloudflare Worker only has D1!&lt;/p&gt;

&lt;p&gt;Also handled — libsql driver works over Turso + Neon's HTTP flavour, and D1 support is on the near roadmap. There's also pgDriver, postgresJsDriver, mysql2Driver, mariadbDriver, planetscaleDriver, mongoDriver, duckdbDriver, mssqlDriver, expoSqliteDriver, opSqliteDriver. Ten adapters. One API.&lt;/p&gt;

&lt;p&gt;Wait — you said no codegen?&lt;/p&gt;

&lt;p&gt;Right, this is the part where Prisma users get suspicious. "No codegen means no types, right?"&lt;/p&gt;

&lt;p&gt;No. It means no prisma generate, no .prisma files, no build step. Types come from the schema you defined in TypeScript directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refunded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoPoint&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;        &lt;span class="c1"&gt;// yes, geo&lt;/span&gt;
  &lt;span class="na"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;      &lt;span class="c1"&gt;// yes, vectors&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Order&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres://...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// db.order is fully typed from the schema you wrote 3 lines up&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// ← "pending" | "paid" | "refunded" enum-narrowed&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;near&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;6.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;withinMeters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notes&lt;/span&gt;            &lt;span class="c1"&gt;// typed as string, from the f.json&amp;lt;{ notes: string }&amp;gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You didn't run a build step. You didn't wait for a codegen to finish. The types just are. TypeScript infers everything from f.string() / f.int() / f.enum([...]).&lt;/p&gt;

&lt;p&gt;I actually really enjoy watching new users try to break this. "But what about relations?" Yeah, those too. f.relation.one() / f.relation.many(). "But what about transactions?" db.$transaction(async tx =&amp;gt; ...). "But what about raw SQL?" db.$queryRaw&lt;code&gt;SELECT ...&lt;/code&gt; — typed.&lt;/p&gt;

&lt;p&gt;Real vector + geo queries, in the same API&lt;/p&gt;

&lt;p&gt;This is the bit I'm proudest of. You know how Prisma has like six extensions to do vector search? forge-orm just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;near&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;emb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nearTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queryEmbedding&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;near&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;_distance&lt;/span&gt;        &lt;span class="c1"&gt;// typed number&lt;/span&gt;

&lt;span class="nx"&gt;And&lt;/span&gt; &lt;span class="nx"&gt;geo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nearby&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cafe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;near&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;withinMeters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;asc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nearTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;lng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lat&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;nearby&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;_distanceMeters&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same syntax on Postgres (with PostGIS), on DuckDB (with spatial), on SQLite (with SpatiaLite), on MongoDB (with $near), and on the browser IDB adapter with pure-JS ray-casting. Six backends, one API.&lt;/p&gt;

&lt;p&gt;OK, sell me on the CLI&lt;/p&gt;

&lt;p&gt;npx forge doctor — hits your DB, checks every model, tells you what's missing:&lt;/p&gt;

&lt;p&gt;✔ Postgres 16.2 · schema "public"&lt;br&gt;
✔ Model "user" — table users OK&lt;br&gt;
⚠ Model "order" — field &lt;code&gt;status&lt;/code&gt; DB has TEXT, schema wants ENUM&lt;br&gt;
✔ 24 indexes match&lt;br&gt;
✖ Extension pgvector — NOT installed (required by orders.emb)&lt;br&gt;
   Run: CREATE EXTENSION vector;&lt;/p&gt;

&lt;p&gt;npx forge push — applies drift. Non-destructive by default, --force-destructive for the scary ones.&lt;/p&gt;

&lt;p&gt;npx forge diff — dry-run of push. Shows exactly what SQL will run.&lt;/p&gt;

&lt;p&gt;npx forge rollback — undoes the last push.&lt;/p&gt;

&lt;p&gt;For browser + Tauri, none of that matters — you call await db.$migrate() at boot and it does the same thing at runtime.&lt;/p&gt;

&lt;p&gt;The real-world proof: Dallio&lt;/p&gt;

&lt;p&gt;I'm not shipping a library and hoping someone tries it. I'm running this in production on my own product.&lt;/p&gt;

&lt;p&gt;Dallio is a POS/inventory SaaS for African SMBs. It has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A backend on Node + hyper-express hitting MongoDB with forge-orm/mongo&lt;/li&gt;
&lt;li&gt;A React frontend that persists an offline event log in forge-orm/indexeddb on web tabs&lt;/li&gt;
&lt;li&gt;A Tauri desktop + mobile shell using forge-orm + tauriSqlDriver over @tauri-apps/plugin-sql&lt;/li&gt;
&lt;li&gt;Same SyncEvent model definition across all three. Same query API. One schema file to change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a shopkeeper's phone loses internet mid-checkout, the sale writes to the local IndexedDB via forge-orm. When Wi-Fi comes back, the desktop till's forge-orm-backed SQLite catches up via a WebSocket. The backend's Mongo catches the same event.&lt;/p&gt;

&lt;p&gt;No adapter code. No translation layer. No two-database-schema sync bug. Prisma cannot do this. Drizzle cannot do this. TypeORM… let's not talk about TypeORM.&lt;/p&gt;

&lt;p&gt;Yes, I know what you're thinking&lt;/p&gt;

&lt;p&gt;"Isn't this just another abstraction leak waiting to happen?" Every adapter has its own capability doctor — if you write a query the backend can't support (say, orderBy: nearTo on a Postgres without PostGIS), the doctor tells you at boot, not at 2am when a customer's till breaks.&lt;/p&gt;

&lt;p&gt;"Why should I trust this over Drizzle?" Drizzle is excellent. It's my second-favourite ORM after mine. Drizzle's schema-first + SQL-close model is beautiful. forge-orm targets a different point on the trade-off curve: I traded some of Drizzle's raw-SQL flexibility for Prisma's ergonomics + Drizzle's edge-friendliness + genuine cross-database portability. Pick the shape that fits your team.&lt;/p&gt;

&lt;p&gt;Getting started&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install forge-orm&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Then 20 lines and you're querying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Todo&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sqlite:./todos.db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Try forge-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a working ORM. Change one line to point it at IndexedDB, or Postgres, or Tauri SQLite. Nothing else in your code moves.&lt;/p&gt;

&lt;p&gt;Links&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;npm: npm install forge-orm — &lt;a href="https://npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;https://npmjs.com/package/forge-orm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/johnsonfash/forge-orm" rel="noopener noreferrer"&gt;https://github.com/johnsonfash/forge-orm&lt;/a&gt; (stars appreciated 🙏)&lt;/li&gt;
&lt;li&gt;Full docs: 60+ deep-dive chapters covering every adapter, geo, vector, JSON path, FTS, migrations, transactions&lt;/li&gt;
&lt;li&gt;472 tests — I don't ship regressions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ship a side project on forge-orm, I want to see it. Tag me: @iamJohnFash. Especially if you've got it running somewhere weird (Deno? Bun? A Raspberry Pi POS?). I collect these.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>prisma</category>
      <category>database</category>
      <category>browser</category>
    </item>
    <item>
      <title>One query language for SQL, Mongo, and the browser: the case for Forge</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:28:35 +0000</pubDate>
      <link>https://dev.to/johnsonfash/one-query-language-for-sql-mongo-and-the-browser-the-case-for-forge-247o</link>
      <guid>https://dev.to/johnsonfash/one-query-language-for-sql-mongo-and-the-browser-the-case-for-forge-247o</guid>
      <description>&lt;p&gt;There's a moment in most TypeScript backend projects when the data layer stops being one thing.&lt;/p&gt;

&lt;p&gt;Maybe you started on Postgres, then a feature shipped where audit events made more sense as a flat Mongo document. Maybe your mobile app needs to work on the train, so you added SQLite in the browser. Maybe a "find me the three nearest locations" endpoint forced you to drop into raw ST_Distance_Sphere() because your ORM didn't know what a geo point was. Maybe you added embeddings for an AI search box and now half your query path is a $queryRaw with a hand-rolled vector cosine clause.&lt;/p&gt;

&lt;p&gt;Each of those moments seems small in isolation. Stacked, they're how a typed data layer becomes four data layers — a Prisma client over here, a Mongoose schema over there, Dexie wrapping IndexedDB, and a folder full of raw SQL strings nobody wants to touch. Each layer has its own concept of "model," its own migration story, its own way of describing a where clause. The code in your services is suddenly half plumbing.&lt;/p&gt;

&lt;p&gt;I built Forge because every other TypeScript ORM I tried forced me to pick one surface and treat the rest as a foreign country.&lt;/p&gt;




&lt;p&gt;(&lt;a href="https://www.npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/forge-orm&lt;/a&gt;)&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that produced it
&lt;/h2&gt;

&lt;p&gt;I was working on a multi-tenant SaaS that had to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run Postgres for transactional state&lt;/li&gt;
&lt;li&gt;Use Mongo for high-volume nested documents that didn't want a schema&lt;/li&gt;
&lt;li&gt;Work fully offline in the browser on a Tauri-packaged client, including a point-of-sale flow that took payments without a network&lt;/li&gt;
&lt;li&gt;Geocode every location and answer nearest-neighbor queries on them&lt;/li&gt;
&lt;li&gt;Eventually add embeddings so an operator could type "blue cotton tee" and find products without exact keyword matches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pick any two of those and the existing ORMs work. Pick three and you start writing glue. Pick all five and you spend more time keeping four data layers in sync than building features.&lt;/p&gt;

&lt;p&gt;Prisma covers Postgres beautifully and treats Mongo as a second-class adapter. It has no browser story at all. Geo is raw SQL. Vector is raw SQL.&lt;/p&gt;

&lt;p&gt;Drizzle is the cleanest TS-native SQL builder I've ever used — but it's SQL-only. No Mongo. No browser. Geo and vector escape into hand-written fragments.&lt;/p&gt;

&lt;p&gt;Kysely has the best types of any query builder, but it's also SQL-only and intentionally not an ORM. You write joins. No relations sugar. No browser.&lt;/p&gt;

&lt;p&gt;TypeORM has Mongo support, but the types degrade to any so often the TypeScript story is barely real, and decorators don't compose with edge runtimes.&lt;/p&gt;

&lt;p&gt;Mongoose handles Mongo well. It does not handle Postgres at all.&lt;/p&gt;

&lt;p&gt;Dexie handles IndexedDB. It does not handle anything else.&lt;/p&gt;

&lt;p&gt;So you stack them. And you write a small in-house abstraction layer on top so service code doesn't have to know which one it's hitting. And that layer eats a month every year just keeping up.&lt;/p&gt;

&lt;p&gt;Forge is what happens when you decide that's not acceptable and try to put one query language under all of it.&lt;/p&gt;




&lt;p&gt;Video: &lt;a href="https://youtu.be/FpOaneoCzpw" rel="noopener noreferrer"&gt;https://youtu.be/FpOaneoCzpw&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What you actually get
&lt;/h2&gt;

&lt;p&gt;*&lt;em&gt;One vocabulary across six dialects&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Forge has adapters for Postgres, MySQL, SQLite, Mongo, DuckDB, and MSSQL. The same query method names work on all six:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Postgres&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Mongo&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// DuckDB (analytical)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's not "compatible-ish." Those are byte-identical service-layer calls. The adapter underneath translates where, limit, orderBy, select, include, groupBy, upsert, transaction, and the rest into the dialect's native form. A Mongo $or becomes a SQL OR. A SQL NOT IN becomes Mongo's $nin. JSON path access becomes -&amp;gt;&amp;gt; on Postgres and dotted-key access on Mongo.&lt;/p&gt;

&lt;p&gt;The point isn't that you'll switch databases (you mostly won't). The point is that when a feature lands that needs the other database, you don't relearn an ORM. You point a different db at the same schema and write the same code.&lt;/p&gt;

&lt;p&gt;The browser is a first-class target&lt;/p&gt;

&lt;p&gt;This is the part I haven't seen elsewhere.&lt;/p&gt;

&lt;p&gt;Forge ships a browser adapter built on sqlite-wasm + OPFS, running in a Web Worker. It mounts as opfs:/your-db.sqlite or opfs-sahpool:/... for synchronous-access-handle mode, or :memory: for tests. There are Vite, Next.js, and Webpack plugins that handle the wasm loading without you wiring it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the browser. Same API as the server.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createBrowserDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opfs:/dallio.sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INV-3308&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4250&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;drafts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The query you wrote for your server's Postgres works in the user's browser against SQLite. You stop maintaining two data layers because the same model is the same model. Offline drafts, offline POS sales, offline product catalogs — all queryable with the same findMany your service code uses, no IndexedDB wrapper, no Dexie schema duplicated from your Postgres schema.&lt;/p&gt;

&lt;p&gt;For a Tauri app or a PWA that needs to keep working when the network drops, this collapses an entire architectural sub-project into a config flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Geo, vector, and JSON path are typed
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Place&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;places&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;geoPoint&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;srid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4326&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1536&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;details&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;openingHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="nl"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Nearest 10 places to a point — Postgres uses PostGIS, MySQL uses&lt;/span&gt;
&lt;span class="c1"&gt;// ST_Distance_Sphere, Mongo uses $near. Same query call.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nearby&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nearTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;point&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;3.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;6.45&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;withinMeters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Vector similarity — same vocabulary as geo&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;similar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nearTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;queryEmbedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;take&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Typed JSON path — no string fragments&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cafes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Place&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;details.tags&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cafe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those require raw SQL. None require a separate vector library. None require a PostGIS adapter package. The results are typed — nearby[0].location is a [number, number], similar[0].embedding is number[], details.tags is string[].&lt;/p&gt;

&lt;p&gt;If you've ever written a cosine_similarity() SQL fragment by hand or tried to thread PostGIS through Prisma raw queries, you know what the absence of this costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  No binary engine, no codegen step
&lt;/h2&gt;

&lt;p&gt;Forge is pure TypeScript. There is no Rust query engine that needs to be downloaded per platform, no prisma generate step in your CI, no .prisma schema file written in a custom DSL.&lt;/p&gt;

&lt;p&gt;Your schema is a TS file. Models are values. Importing them gives you typed query methods. That's the whole setup.&lt;/p&gt;

&lt;p&gt;Cold start on Lambda or Vercel Functions is under 50ms, the same as Drizzle and Kysely, an order of magnitude faster than Prisma's historical baseline. Edge runtimes (Workers, Vercel Edge, Bun, Deno) work natively because nothing about Forge requires Node-specific APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drift detection that works in production
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// {&lt;/span&gt;
&lt;span class="c1"&gt;//   alteredColumns: [{ table: "users", column: "phone", action: "added" }],&lt;/span&gt;
&lt;span class="c1"&gt;//   pending: [{ table: "users", column: "legacy_id", action: "drop" }],&lt;/span&gt;
&lt;span class="c1"&gt;// }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;db.$migrate()&lt;/code&gt; walks the live schema against the model definitions. Safe drift (an added column, a new index) gets applied automatically. Destructive drift (a column that no longer exists in the model) is surfaced under pending for your migration tool to handle deliberately. You can opt out with db.$migrate({ alter: false }).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;db.$diff(driver, { schema })&lt;/code&gt; returns the same report without applying anything. db.$doctor() runs a live probe — checks extensions, indexes, encoding, latency — and tells you what's wrong. Both work in the browser too, against the user's OPFS SQLite.&lt;/p&gt;

&lt;p&gt;In practice this means the day a new feature ships with two new columns, you don't need to ship a migration file. You can let $migrate() apply the additions on boot and ship a migration for the destructive part next sprint. Or, if you're more conservative, you can run $diff() in CI and refuse to deploy when drift is detected. Both flows work.&lt;/p&gt;

&lt;p&gt;Atomic upsert that actually is atomic&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;scopedDb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lastSeenAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That call compiles to INSERT ... ON CONFLICT DO UPDATE on Postgres, INSERT ... ON DUPLICATE KEY UPDATE on MySQL, MERGE on MSSQL, &lt;/p&gt;

&lt;p&gt;&lt;code&gt;findOneAndUpdate({ upsert: true })&lt;/code&gt; on Mongo, and the SQLite equivalent. One round trip. No find-then-branch race condition. No accidental double-insert under concurrency.&lt;/p&gt;

&lt;p&gt;I mention this because it's not glamorous, but every ORM that doesn't do it correctly produces a class of intermittent production bugs you can never reproduce locally and your tracing tool blames on "user double-clicked." Forge does it correctly across all six dialects.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Forge is not
&lt;/h2&gt;

&lt;p&gt;I'll be direct about this because honest tradeoffs are part of why anyone trusts a recommendation.&lt;/p&gt;

&lt;p&gt;There is no Studio. Prisma Studio is a category-defining piece of tooling. db.$doctor is CLI-only and doesn't browse data. If your team includes people who want to click around tables in a GUI, Forge will frustrate them until a Studio ships.&lt;/p&gt;

&lt;p&gt;There is no SaaS company behind it. That has good and bad sides. No paid Accelerate/Pulse/Optimize tier means the OSS roadmap isn't shaped by upsell pressure. No SaaS company also means no support contract, no enterprise SLA, no Salesforce conference booth, no team of dedicated maintainers if I get hit by a bus.&lt;/p&gt;

&lt;p&gt;The community is tiny. If you Google a Forge error, you'll mostly find the README. Stack Overflow won't help. There are no YouTube tutorials. Onboarding a junior engineer takes longer because the answer to "how do I do X" is "read the source," not "watch this 20-minute video."&lt;/p&gt;

&lt;p&gt;Depth versus breadth. Drizzle has gone deeper into Postgres-specific edge cases than Forge has. Mongoose has gone deeper into Mongo's aggregation pipeline. Forge covers six dialects well, but a dialect-specific ORM will always go deeper into its one dialect. If you need Postgres logical replication consumer slots or Mongo's exact change-stream resume semantics surfaced as a first-class API, you'll write that integration yourself in Forge.&lt;/p&gt;

&lt;p&gt;It's young. First release was 2024. 2.5.3 is the current version. The shape is stable, the test suite is large (the test file count crossed 470 in 2.5.3, mostly per-dialect parity), but five-year battle scars accumulate things you can't get faster. Expect a few rough edges.&lt;/p&gt;

&lt;p&gt;If those are dealbreakers, Drizzle or Prisma is your answer. That's a real choice, not a consolation prize.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Forge is the right answer
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Pick Forge when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Your data already lives in more than one place. Postgres + Mongo. Postgres + SQLite (mobile sync). DuckDB for analytics over a transactional Postgres. MSSQL for a legacy system plus Postgres for the new one. Forge collapses the abstraction tax of running multiple ORMs.&lt;/li&gt;
&lt;li&gt;You're building an offline-first or local-first app. Tauri desktop apps, Capacitor mobile apps, PWAs that need to work on flaky connections. Forge's browser adapter means you maintain one data layer instead of one server data layer plus one client IndexedDB layer plus the sync code between them.&lt;/li&gt;
&lt;li&gt;Geo or vector is on your roadmap. Even if you don't need them today, building them on raw SQL fragments is a permanent tax. Forge surfaces both as typed primitives.&lt;/li&gt;
&lt;li&gt;You care about cold starts. Lambda, Vercel Functions, Cloudflare Workers — Forge is pure TS, no binary, no codegen runtime. Drizzle and Kysely match it; Prisma still doesn't.&lt;/li&gt;
&lt;li&gt;You like writing code more than configuration. The schema is a TS file. The migration story is "the live schema and the models disagree, here's what changed." There's no DSL, no codegen step, no separate migration CLI to learn.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Don't pick Forge when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You have a senior Postgres-only team that already loves Drizzle. Forge wouldn't make their day better.&lt;/li&gt;
&lt;li&gt;You need Studio and your stakeholders aren't going to budge.&lt;/li&gt;
&lt;li&gt;You're shipping inside a company that requires a vendor with a support contract.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm install forge-orm@^2.5.3&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Define a model file:
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// schema.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;unique&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pro&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;enterprise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;defaultNow&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Connect from your server:
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPostgresDb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm/postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createPostgresDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// applies safe drift on boot&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or from the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createBrowserDb&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forge-orm/browser&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createBrowserDb&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opfs:/app.sqlite&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$migrate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same db.User.findMany({ where: { tier: "pro" } }) works in both. That is the entire setup.&lt;/p&gt;




&lt;p&gt;A statement about TypeScript data layers in 2026&lt;/p&gt;

&lt;p&gt;The data layer should follow your data, not the other way around. When a feature pushes you toward a different database — analytical, document, offline — the ORM should come with you, not force you to maintain a second one.&lt;/p&gt;

&lt;p&gt;Most TypeScript ORMs were built when "the database" meant Postgres or maybe MySQL. They optimize for the depth of that single relationship. That's an honest tradeoff and it produces excellent tools for the case where it's correct.&lt;/p&gt;

&lt;p&gt;But TypeScript apps in 2026 aren't single-database anymore. They run partially in the browser. They store transactional data in one engine and analytical data in another. They pull embeddings out of vector indexes and locations out of geo indexes. They sync some state offline and reconcile it when the network returns.&lt;/p&gt;

&lt;p&gt;Forge is what an ORM looks like if you take that reality as the starting point.&lt;/p&gt;

&lt;p&gt;It is not the safe choice. The safe choice is Prisma. The cool choice is Drizzle. The purist choice is Kysely.&lt;/p&gt;

&lt;p&gt;Forge is the choice when none of those covers your data's actual shape, and you'd rather pay the cost of a younger tool than the cost of running four mature ones in parallel for the rest of the project's life.&lt;/p&gt;

&lt;p&gt;If that's where you are, forge-orm@^2.5.3 (&lt;a href="https://www.npmjs.com/package/forge-orm" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/forge-orm&lt;/a&gt;) is one npm install away.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>sql</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How I Built a Chrome Extension to Extract Emails and Phone Numbers – A Developer’s Journey</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Sat, 29 Nov 2025 08:55:17 +0000</pubDate>
      <link>https://dev.to/johnsonfash/how-i-built-a-chrome-extension-to-extract-emails-and-phone-numbers-a-developers-journey-5f6a</link>
      <guid>https://dev.to/johnsonfash/how-i-built-a-chrome-extension-to-extract-emails-and-phone-numbers-a-developers-journey-5f6a</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zrcfttbqnqnw0zylv30.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4zrcfttbqnqnw0zylv30.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8we7rf5aacpg6alhc7k.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj8we7rf5aacpg6alhc7k.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xfi483xbhessinxcc8i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7xfi483xbhessinxcc8i.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a fullstack developer, I’m always on the lookout for tools that save time and automate repetitive tasks. One thing I noticed early in my career is that collecting contact information—emails and phone numbers—from websites can be tedious, especially if you’re doing it manually. Whether it’s for outreach, research, or building a curated contact list, manually scanning pages is slow, error-prone, and frankly, exhausting.&lt;/p&gt;

&lt;p&gt;That’s when I decided to create my very first Chrome extension: an Email and Phone Extractor. This project wasn’t just about scraping data; it was about building a lightweight, user-friendly, and reliable tool that anyone could use ethically and efficiently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chromewebstore.google.com/detail/email-phone-extractor/oflljapdhcnlfapfammamnieoknpfhfj" rel="noopener noreferrer"&gt;You can add the Extension to your chrome browser here!&lt;br&gt;
&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built This Extension
&lt;/h2&gt;

&lt;p&gt;The idea came from my own workflow frustrations. I often needed contact information from company websites, portfolio pages, or public directories. Copying and pasting each email or number was draining. I wanted a tool that would scan the page automatically, organize the results, and give me a clean output I could use immediately.&lt;/p&gt;

&lt;p&gt;I envisioned a simple Chrome extension that could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect all email addresses and phone numbers on the current page.&lt;/li&gt;
&lt;li&gt;Provide a clear interface to view, copy, or save the extracted contacts.&lt;/li&gt;
&lt;li&gt;Handle dynamically loaded content without slowing down the browser.&lt;/li&gt;
&lt;li&gt;Be easy for anyone to install and use, even without a technical background.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting Started with Chrome Extension Development
&lt;/h2&gt;

&lt;p&gt;Building a Chrome extension was a new adventure for me. I started by diving into Chrome’s extension APIs. Chrome extensions have three main components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Manifest file – defines the extension’s metadata and permissions.&lt;/li&gt;
&lt;li&gt;Content scripts – allow the extension to interact with the web page’s DOM.&lt;/li&gt;
&lt;li&gt;Background scripts – handle messaging, persistent storage, and long-running tasks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Building a Clean and Intuitive Interface
&lt;/h2&gt;

&lt;p&gt;One of my main goals was user experience. I wanted the extension to be simple but functional. The popup displays:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Emails and phone numbers in separate sections&lt;/li&gt;
&lt;li&gt;A button to copy all results to the clipboard&lt;/li&gt;
&lt;li&gt;A count of unique emails and phone numbers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Optional filters to remove duplicates&lt;/p&gt;

&lt;p&gt;I wanted users to feel in control. They can extract all contacts with a single click and copy them without dealing with messy page content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethical Considerations
&lt;/h2&gt;

&lt;p&gt;While the extension can extract public information, I wanted to make ethical use clear. I added a disclaimer reminding users to respect privacy laws and not misuse the tool for spam. Emails and phone numbers collected should only be used for legitimate purposes, like reaching out professionally or conducting research.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges Along the Way
&lt;/h2&gt;

&lt;p&gt;Building this extension wasn’t without hurdles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Handling large pages: Some websites have thousands of DOM nodes. I had to ensure the extension scanned efficiently without freezing the browser.&lt;/li&gt;
&lt;li&gt;Dynamic content: Modern sites often load data asynchronously. Mutation observers were critical here.&lt;/li&gt;
&lt;li&gt;False positives: Regex sometimes matched text that looked like emails or phone numbers but weren’t. I added filtering and validation to minimize this.&lt;/li&gt;
&lt;li&gt;Cross-browser quirks: While building primarily for Chrome, I ensured that the extension’s logic could be adapted for other Chromium-based browsers like Edge and Brave.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-Life Use Cases
&lt;/h2&gt;

&lt;p&gt;Here are some ways developers and professionals can benefit from this extension:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sales and Business Development: Quickly gather leads from public directories and websites.&lt;/li&gt;
&lt;li&gt;Recruitment: Extract contact information of potential candidates from company pages or LinkedIn profiles (respecting platform policies).&lt;/li&gt;
&lt;li&gt;Networking: Build professional connections by compiling contact lists for outreach.&lt;/li&gt;
&lt;li&gt;Research &amp;amp; Data Collection: Academics and researchers can collect emails for surveys or collaborations.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because of these use cases, the extension isn’t just a productivity tool—it’s a way to automate routine tasks and focus on what truly matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing My Work
&lt;/h2&gt;

&lt;p&gt;After testing extensively and refining the features, I published the extension on the Chrome Web Store. The process taught me a lot about packaging, metadata, and compliance with Google’s publishing rules. I also shared it with friends and colleagues, who found it immediately useful.&lt;/p&gt;

&lt;p&gt;For those interested, you can try it here: Email &amp;amp; Phone Extractor Chrome Extension&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building this Chrome extension was more than a coding exercise—it was a lesson in user experience, problem-solving, and ethical development. Some key takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Even small projects can have complex technical challenges&lt;/li&gt;
&lt;li&gt;Regex is powerful but requires careful testing&lt;/li&gt;
&lt;li&gt;Handling dynamic content is critical for modern web tools&lt;/li&gt;
&lt;li&gt;Ethical considerations should never be overlooked&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Creating the Email and Phone Extractor Chrome extension was a fulfilling journey. It started as a personal tool to save time and evolved into a polished extension that can help other developers, researchers, and professionals streamline their workflows.&lt;/p&gt;

&lt;p&gt;If you’re a developer looking for a practical project, Chrome extension development is a fantastic way to combine front-end skills with practical problem-solving. And who knows—you might just build a tool that thousands of people end up using!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>productivity</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Email &amp; Phone Extractor — One-Click Contact Collection for Any Website</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Tue, 25 Nov 2025 10:04:03 +0000</pubDate>
      <link>https://dev.to/johnsonfash/email-phone-extractor-one-click-contact-collection-for-any-website-4f8d</link>
      <guid>https://dev.to/johnsonfash/email-phone-extractor-one-click-contact-collection-for-any-website-4f8d</guid>
      <description>&lt;p&gt;A friend of mine spent hours manually collecting emails and phone numbers from websites. If you have, then you know it can drive one insane.&lt;/p&gt;

&lt;p&gt;After watching a friend on a google meet “speed-run” through LinkedIn, and opened a couple of company websites for lead generation, I realized something: the tools that automate this were either too expensive or too limited.&lt;/p&gt;

&lt;p&gt;So, as a developer, i built a solution — a Chrome extension that extracts emails and phone numbers from any webpage with just one click.&lt;/p&gt;

&lt;p&gt;Here’s the story behind it, how it works, and why it could change the way you handle web-based contacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Story Behind Email &amp;amp; Phone Extractor
&lt;/h2&gt;

&lt;p&gt;About two months ago, a friend of mine, a digital marketer, was giving me tips on LinkedIn optimization.&lt;/p&gt;

&lt;p&gt;While talking, he opened Google Maps, lists of companies, multiple website tabs, and internal links — basically navigating a maze while multitasking. I asked him,&lt;/p&gt;

&lt;p&gt;&lt;code&gt;“Bro, why is this so manual?”&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
He laughed and said:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;“The extensions that automate this are too expensive for Nigerians.”&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
That one sentence sparked an idea. I love turning repetitive, manual tasks into clean, one-click solutions. That night, I started building the first version of Email &amp;amp; Phone Extractor, and now it’s a real Chrome extension that works today.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Install the Extension&lt;br&gt;
Available on the Chrome Web Store. One click installs it in seconds.&lt;/p&gt;

&lt;p&gt;Navigate to Any Page&lt;br&gt;
Works on LinkedIn, WhatsApp Web, company websites, and almost any Single Page Application (SPA).&lt;/p&gt;

&lt;p&gt;Click the Extension Icon&lt;br&gt;
Instantly extracts emails and phone numbers visible on the page, including content loaded dynamically via JavaScript.&lt;/p&gt;

&lt;p&gt;View or Export Data&lt;br&gt;
Export contacts as JSON or copy to clipboard. Manage them on your local machine or via our optional account sync feature.&lt;/p&gt;

&lt;p&gt;Handles Complex Sites&lt;br&gt;
Unlike simple scrapers, this tool digs into local and session storage, IndexedDB, and cached data to extract contacts even when they’re hidden from the DOM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built It
&lt;/h2&gt;

&lt;p&gt;The mission is simple: save time, reduce frustration, and make web contact collection accessible to everyone.&lt;/p&gt;

&lt;p&gt;Whether you’re a marketer, sales professional, researcher, or just curious, this extension turns tedious manual scraping into a clean, one-click workflow.&lt;/p&gt;

&lt;p&gt;Speed: Collect data in seconds, not hours.&lt;/p&gt;

&lt;p&gt;Versatility: Works on almost any site.&lt;/p&gt;

&lt;p&gt;Accessibility: No expensive subscriptions or complex setups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Video
&lt;/h2&gt;

&lt;p&gt;Watch it in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/GWFwXpZ9Tl4" rel="noopener noreferrer"&gt;https://youtu.be/GWFwXpZ9Tl4&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Screenshots
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40jkck36497s8x5erytt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F40jkck36497s8x5erytt.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3uyn76b8vezrjihg17ax.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3uyn76b8vezrjihg17ax.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1rvwfqcb91wpdsj1i7yh.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1rvwfqcb91wpdsj1i7yh.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttcdlwt4xlh3rzfexgp3.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fttcdlwt4xlh3rzfexgp3.jpg" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;Install the Chrome extension, and start extracting emails and phone numbers in one click:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://chromewebstore.google.com/detail/email-phone-extractor/oflljapdhcnlfapfammamnieoknpfhfj" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://contact-extractor.tdigital.ng" rel="noopener noreferrer"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>javascript</category>
      <category>extensions</category>
    </item>
    <item>
      <title>Let Me Introduce to You My Portfolio Website</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Mon, 26 Apr 2021 14:45:59 +0000</pubDate>
      <link>https://dev.to/johnsonfash/let-me-introduce-to-you-my-portfolio-website-4p52</link>
      <guid>https://dev.to/johnsonfash/let-me-introduce-to-you-my-portfolio-website-4p52</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Hello Everyone,&amp;nbsp;&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;I have finally &lt;strong&gt;completed&lt;/strong&gt; the built of my &lt;strong&gt;portfolio website&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Features&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; Static Landing Page with CSS,HTML and JavaScript Custom Animation&lt;/li&gt;
&lt;li&gt; Blog Posts&lt;/li&gt;
&lt;li&gt; Custom Management System (WordPress Clone) with SEO Manager&lt;/li&gt;
&lt;li&gt; Project Demo and more&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the Link: &lt;a href="https://apluswebmaker.com"&gt;https://apluswebmaker.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>portfolio</category>
      <category>website</category>
    </item>
    <item>
      <title>JavaScript On Scroll Animation Library</title>
      <dc:creator>Johnson Fash</dc:creator>
      <pubDate>Thu, 22 Apr 2021 07:51:59 +0000</pubDate>
      <link>https://dev.to/johnsonfash/javascript-on-scroll-animation-library-3jh9</link>
      <guid>https://dev.to/johnsonfash/javascript-on-scroll-animation-library-3jh9</guid>
      <description>&lt;h1&gt;
  
  
  &lt;strong&gt;OnScroll Animation&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://travis-ci.com/johnsonfash/scroll-animation"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m1WgLos8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://travis-ci.com/johnsonfash/scroll-animation.svg%3Fbranch%3Dmaster" alt="Build Status"&gt;&lt;/a&gt; &lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ES67nC1M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://img.shields.io/github/package-json/v/johnsonfash/scroll-animation" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ES67nC1M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://img.shields.io/github/package-json/v/johnsonfash/scroll-animation" alt="GitHub package.json version"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GQFBwHZi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://img.shields.io/twitter/follow/iamJohnsonFash%3Fstyle%3Dsocial" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GQFBwHZi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://img.shields.io/twitter/follow/iamJohnsonFash%3Fstyle%3Dsocial" alt="Twitter Follow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Scroll Animation Library is a simple JavaScript library for animation when element(s) are in view while scrolling the browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 &lt;strong&gt;&lt;a href="https://johnsonfash.github.io/onscroll-animation/website.html"&gt;Demo&lt;/a&gt;&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://johnsonfash.github.io/onscroll-animation/website.html"&gt;Custom website build&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://johnsonfash.github.io/onscroll-animation/3d-box.html"&gt;3D box animation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://johnsonfash.github.io/onscroll-animation/articles.html"&gt;Article Slides&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  ⚙ &lt;strong&gt;Installation&lt;/strong&gt;
&lt;/h1&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Option A.&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;NPM installation&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;npm install onscroll-animation --save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Import:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onscroll-animation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Option B.&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use CDN - load directly from&amp;nbsp;jsDelivr CDN&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/onscroll-animation@latest/dist/animate.bundle.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;


&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;animate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;script&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  &lt;strong&gt;Use&lt;/strong&gt;
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;animate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.grid11&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-duration: 0.8s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-delay: 1s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-fill-mode: forwards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transform: translateX(-150px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section.one .left, section.three .book, section.five .other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left: -500px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;left&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section.one .right, section.three .complex, section.five .person&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right: -500px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right: 0px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;section.two&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opacity: 0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transform: translateY(100px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opacity: 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transform: translateY(0px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.grid10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;
          &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-duration: 0.8s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-fill-mode: forwards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;transform: translateY(-110px)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultParam&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-duration: .8s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run: once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-fill-mode: forwards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pixel-correction: -100px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-time-function: ease-out&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nx"&gt;animate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  &lt;strong&gt;Explanation&lt;/strong&gt;
&lt;/h1&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;OnScrollAnimation&amp;nbsp;class&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;OnScrollAnimation({....})&lt;/code&gt;&lt;/strong&gt; accept only an object &lt;strong&gt;&lt;code&gt;{...}&lt;/code&gt;&lt;/strong&gt;. This object contains css selectors like &lt;strong&gt;&lt;code&gt;".grid10"&lt;/code&gt;&lt;/strong&gt;,&amp;nbsp;&lt;strong&gt;&lt;code&gt;"section.two img, section.four img"&lt;/code&gt;&lt;/strong&gt; etc.&lt;/p&gt;

&lt;p&gt;Basically, this object properties can be any css selector, which a &lt;strong&gt;&lt;code&gt;document.querySelector()&lt;/code&gt;&lt;/strong&gt; method accepts.&lt;/p&gt;

&lt;p&gt;The value for the &lt;strong&gt;CSS Selector&lt;/strong&gt; i.e &lt;strong&gt;&lt;code&gt;".grid4"&lt;/code&gt;&lt;/strong&gt; must be an object which holds various &lt;strong&gt;properties&lt;/strong&gt; and &lt;strong&gt;values&lt;/strong&gt; for animation to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Properties&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. `&lt;/strong&gt;parameters:[...]&lt;strong&gt;&lt;code&gt;&amp;nbsp; or &lt;/code&gt;parameters: {...}` ;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This define &lt;strong&gt;&lt;code&gt;@keyframes&lt;/code&gt;&lt;/strong&gt; property for each element i.e&amp;nbsp;&lt;code&gt;parameters: [...]&lt;/code&gt;&amp;nbsp;or&amp;nbsp;&lt;code&gt;parameters: {...)&lt;/code&gt; can be an array containing strings of regular css or object containing its javascript equivalent like the example below:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;run&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;run&lt;/code&gt; can be omitted or included. This lets you determine if animation runs &lt;code&gt;once&lt;/code&gt; or continous anytime an animated element is in view.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pixelCorrection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pixel-correction&lt;/code&gt; or &lt;code&gt;pixelCorrection&lt;/code&gt; use to make correction (in pixel) to when animation starts for an element. i.e &lt;code&gt;100px&lt;/code&gt; means scroll &lt;code&gt;100px&lt;/code&gt; downward before animation starts for an element in viewport, and &lt;code&gt;-100px&lt;/code&gt; the opposite.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-duration: 1s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-delay: 2s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-fill-mode: forwards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-time-function: ease-in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pixel-correction: -200px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// makes correction to how far down or up to go before element in view animates&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run: once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;//can be ommited. default is to run everytime element is in view&lt;/span&gt;
    &lt;span class="p"&gt;..........&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;using&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;

  &lt;span class="nx"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animationDuration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;animationDelay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;animationFillMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forwards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;animationTimeFunction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ease-in&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pixelCorrection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-200px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;..........&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  NOTE:
&lt;/h2&gt;

&lt;p&gt;There is non shortcut like &lt;strong&gt;&lt;code&gt;"animation: drop 1s forwards"&lt;/code&gt;&lt;/strong&gt; for now. Please specifically list out your @keyframes by name and function like in the example above.&lt;/p&gt;

&lt;p&gt;Properties of a selector i.e &lt;strong&gt;&lt;code&gt;parameters&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;**from**&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;to&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;0%&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;**75%**&lt;/code&gt; and more can both be an array, containing string equivalent of your regular css property or an object containing its equivalent in javascript. i.e &lt;strong&gt;"max-width"&lt;/strong&gt; is &lt;strong&gt;maxWidth&lt;/strong&gt; when working with objects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;from: [...]&lt;/code&gt; or &lt;code&gt;from:{...}&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Similar to css property &lt;code&gt;from {.....}&lt;/code&gt; used in &lt;code&gt;@keyframe&lt;/code&gt;. i.e &lt;code&gt;from: ["width: 0px","height:20px"....]&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;to: [...]&lt;/code&gt; or &lt;code&gt;to: {....}&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Similar to css property &lt;code&gt;to: {.....}&lt;/code&gt; used in &lt;code&gt;@keyframe&lt;/code&gt; after defining &lt;code&gt;from {...}&lt;/code&gt; i.e &lt;code&gt;to: {width: "100%",height: "200px"}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;0: [...], 50: [...], 100:{.....}&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is similar to using percentage in &lt;code&gt;@keyframes&lt;/code&gt;, only difference is not including the &lt;code&gt;%&lt;/code&gt; sign i.e&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#imag1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[.....],&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;width: 20px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.....],&lt;/span&gt;
    &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[......],&lt;/span&gt;
    &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[.....]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;..........&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  &lt;strong&gt;Using custom css&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Without defining animation @keyframes in javascript, custom css can be used with each element by including a &lt;code&gt;class&lt;/code&gt; that defines the &lt;code&gt;@keyframe&lt;/code&gt; in your stylesheet i.e&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;"ball"&lt;/span&gt; &lt;span class="na"&gt;scr=&lt;/span&gt;&lt;span class="s"&gt;"./asset/ball.jpg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"image1"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ballmove&lt;/span&gt; &lt;span class="m"&gt;1s&lt;/span&gt; &lt;span class="n"&gt;forwards&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;ballmove&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nt"&gt;from&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-100px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;to&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.image1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;move&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;// adds custom css class only&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;img&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;css&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;bounce&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Animation.defaultParams()&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The Animation method &lt;code&gt;defaultParams()&lt;/code&gt; defines a default paramter for each selector. Meaning you can ommit the parameters property for every element if they are all thesame i.e&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.grid1, .grid2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[....],&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[....]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.grid4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
    &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;........&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultParams&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-duration: 1s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-fill-mode: forwards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// or animation.defaultParams({animationDuration: "2s".......});&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also &lt;strong&gt;ovaride&lt;/strong&gt; the &lt;code&gt;defaultParams()&lt;/code&gt; method for an element by specifying its own &lt;code&gt;parameters&lt;/code&gt; i.e&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;OnScrollAnimation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.grid1, .grid2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[....],&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[....]&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.grid4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[....]&lt;/span&gt;  &lt;span class="c1"&gt;// override defaultParams&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...],&lt;/span&gt;
    &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;........&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultParams&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-duration: 1s&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;animation-fill-mode: forwards&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// or animation.defaultParams({animationDuration: "2s".......});&lt;/span&gt;
&lt;span class="nx"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;Animation.init()&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;init()&lt;/code&gt; method initialize the animation to run after the page loads.&lt;/p&gt;

&lt;h1&gt;
  
  
  More Example:
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const animation = new OnScrollAnimation({
        ".one": {
          parameters: [
            "animation-duration: 1s",
            "pixel-correction: -100px",
            "animation-delay: .5s",
            "animation-time-function: linear"
          ],
          from: [
            "transform: translateY(-1000px)"
          ],
          to: [
            "transform: translateY(0px)"
          ]
        },
        ".two": {
          parameters: [
            "animation-duration: 1s",
            "pixel-correction: -300px"
          ],
          from: {
            transform: "rotate(0deg)"
          },
          to: [
            "transform: rotate(360deg)"
          ]
        },
        "article.three": {
          parameters: {
            animationDuration: "1s",
            animationFillMode: "forwards",
            animationTimingFunction: "ease-in"
          },
          0: [
            "transform: translateX(-1000px)",
          ],
          50: [
            "transform: translateX(1000px)",
            "background-color: red"
          ],
          100: [
            "transform: translateX(0px)"
          ]
        },
        ".four": {
          parameters: [
            "animation-duration: 1s"
          ],
          from: [
            "transform: skewX(20deg) translateX(-1000px)"
          ],
          to: [
            "transform: skewX(0deg) translateX(0px)"
          ]
        },
        ".five": {
          parameters: [
            "animation-duration: 1s"
          ],
          from: [
            "position: relative",
            "right: -1000px",
            "transform: skewX(-20deg)"
          ],
          to: [
            "position: relative",
            "right: 0px",
            "transform: skewX(0deg)"
          ]
        },
        ".six": {
          parameters: [
            "animation-duration: 2s",
            "animation-fill-mode: forwards",
          ],
          0: [
            "transform: translateY(0)"
          ],
          75: [
            "transform: translateY(50vh)"
          ]
        },
        ".seven": {
          parameters: [
            "animation-duration: 1.5s"
          ],
          from: [
          "transform: rotateY(0deg)"
          ],
          to: [
          "transform: rotateY(360deg)"
          ]
        }
      });
      animation.init();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>javascript</category>
      <category>html</category>
      <category>css</category>
      <category>npm</category>
    </item>
  </channel>
</rss>
