<rss version="2.0">
  <channel>
    <title>Turso on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/turso/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Wed, 08 Jul 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>Geni vs. Goose for lightweight database migrations</title>
      <link>https://llbbl.blog/2026/07/08/geni-vs-goose-for-lightweight.html</link>
      <pubDate>Wed, 08 Jul 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/07/08/geni-vs-goose-for-lightweight.html</guid>
      <description>&lt;p&gt;I have a pretty simple rule for database migrations: the tool lets me write SQL, check status, apply the next change, and roll back when I need to. I don&amp;rsquo;t want much more standing in the way.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why I like both Geni and Goose.&lt;/p&gt;
&lt;p&gt;Neither one is trying to be a full application framework. They don&amp;rsquo;t require the rest of the app to be written in the same language as the migration tool. They&amp;rsquo;re small enough to understand, script, and run from CI, but they have enough features that they don&amp;rsquo;t feel like a pile of shell scripts once a project grows real production databases.&lt;/p&gt;
&lt;p&gt;The catch is that they&amp;rsquo;re good at slightly different jobs. I&amp;rsquo;ve ended up using both in the same migration repo: Goose for PostgreSQL, MySQL, and MariaDB, Geni for LibSQL/Turso. That split has worked well enough that I&amp;rsquo;d reach for the same shape again, even on projects that aren&amp;rsquo;t written in Go or Rust.&lt;/p&gt;
&lt;h2 id=&#34;the-short-version&#34;&gt;The short version&lt;/h2&gt;
&lt;p&gt;Use &lt;strong&gt;Goose&lt;/strong&gt; when you want a mature, widely used SQL migration tool for the traditional server databases: PostgreSQL, MySQL, MariaDB, and local SQLite. Broad database matrix, familiar single-file format, status and rollback commands, environment-variable substitution, Go migrations when you need them, and a very plain CLI.&lt;/p&gt;
&lt;p&gt;Use &lt;strong&gt;Geni&lt;/strong&gt; when LibSQL or Turso is part of the system. It&amp;rsquo;s a standalone tool with first-class LibSQL/Turso support, a simple paired-file model, status/up/down/new commands, and optional schema snapshots. It was built for the space where &amp;ldquo;SQLite-like, but remote and edge-hosted&amp;rdquo; starts to expose the rough edges of tools designed around local SQLite files.&lt;/p&gt;
&lt;p&gt;Reach for something heavier when migrations stop being ordered SQL files and turn into a schema-management problem. More on that at the end.&lt;/p&gt;
&lt;h2 id=&#34;why-i-like-both&#34;&gt;Why I like both&lt;/h2&gt;
&lt;p&gt;The best thing about Geni and Goose is that the migration stays close to the database.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no ceremony around a model layer. You write SQL. You put it in version control. You run a CLI. The database gets a tracking table. A teammate can review the migration in a pull request without learning your ORM. A deploy script can ask for status before it applies anything. For a lot of small and medium projects, that&amp;rsquo;s exactly the right amount of machinery.&lt;/p&gt;
&lt;p&gt;It also makes both tools language-independent in practice. Goose is written in Go, Geni in Rust, but a TypeScript, Python, Ruby, or PHP app can use either one just fine. The migration tool runs at the edge of the application. It talks to the database and the filesystem. It doesn&amp;rsquo;t need to be imported by the app.&lt;/p&gt;
&lt;p&gt;That separation is underrated. A migration directory can outlive a framework rewrite. It can be shared by several services, live in an infrastructure repo, and get run by CI, a deploy script, or a human with a terminal.&lt;/p&gt;
&lt;h2 id=&#34;where-goose-shines&#34;&gt;Where Goose shines&lt;/h2&gt;
&lt;p&gt;Goose is the boring choice, in the best sense. Its migration format is easy to read:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- +goose Up
&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- +goose StatementBegin
&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; users (
  id BIGSERIAL &lt;span style=&#34;color:#66d9ef&#34;&gt;PRIMARY&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;KEY&lt;/span&gt;,
  email TEXT &lt;span style=&#34;color:#66d9ef&#34;&gt;NOT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;NULL&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;UNIQUE&lt;/span&gt;
);
&lt;span style=&#34;color:#75715e&#34;&gt;-- +goose StatementEnd
&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;
&lt;span style=&#34;color:#75715e&#34;&gt;-- +goose Down
&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;-- +goose StatementBegin
&lt;/span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;DROP&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;TABLE&lt;/span&gt; users;
&lt;span style=&#34;color:#75715e&#34;&gt;-- +goose StatementEnd
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The forward and rollback changes live in one file, and a review comment can talk about both in one place.&lt;/p&gt;
&lt;p&gt;Goose is also strong when a repo has several conventional engines. It documents support for PostgreSQL, MySQL, MariaDB, SQLite, and more, and the command shape stays mostly the same across them:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;goose -dir migrations/postgres/app/schema postgres &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$POSTGRES_APP_DSN&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; up
goose -dir migrations/mysql/app/schema mysql &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;$MYSQL_APP_DSN&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt; up
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For Go services there&amp;rsquo;s another useful escape hatch: Go migrations. Most schema changes should stay in SQL, but occasionally one needs batching, custom validation, or a data rewrite that&amp;rsquo;s clearer in code. Goose supports that without forcing every migration to become code.&lt;/p&gt;
&lt;p&gt;I reach for Goose first on Postgres/MySQL/MariaDB, on teams that already know SQL migrations, and in deploy scripts that just want &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, and &lt;code&gt;version&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;where-geni-shines&#34;&gt;Where Geni shines&lt;/h2&gt;
&lt;p&gt;Geni has a similar direct feel, but its file model is different:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;migrations/
  1782054288_create_users_table.up.sql
  1782054288_create_users_table.down.sql
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Forward migration in one file, rollback in another, both plain SQL. Geni can create the pair for you:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;geni new create_users_table
geni status
geni up
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The more important point is LibSQL/Turso. Turso is not just &amp;ldquo;a local SQLite file with a different name.&amp;rdquo; LibSQL is SQLite-compatible, but a remote Turso database brings its own connection shape, auth token, protocol, and operational behavior. That&amp;rsquo;s exactly where a migration tool either feels native or feels like you&amp;rsquo;re pushing it through a small opening.&lt;/p&gt;
&lt;p&gt;Geni expects the Turso shape. The documented flow uses &lt;code&gt;DATABASE_URL&lt;/code&gt; and &lt;code&gt;DATABASE_TOKEN&lt;/code&gt;, it speaks LibSQL, and it can dump a &lt;code&gt;schema.sql&lt;/code&gt; snapshot after migrations, which is handy as a review artifact and source-control reference.&lt;/p&gt;
&lt;p&gt;On my own Turso projects I like a thin wrapper around Geni. It resolves secrets from whatever store the project uses, splits a composed DSN into &lt;code&gt;DATABASE_URL&lt;/code&gt; and &lt;code&gt;DATABASE_TOKEN&lt;/code&gt;, points Geni at the right per-database folder, and masks tokens in debug output. That keeps the human command simple:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;mise run geni dev app status
mise run geni prod app up
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One folder per logical database, paired up/down files, secrets outside the repo, a status check before deploy, and a confirmation prompt before production &lt;code&gt;up&lt;/code&gt; or &lt;code&gt;down&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;about-goose-and-turso&#34;&gt;About Goose and Turso&lt;/h2&gt;
&lt;p&gt;This is the one place the recommendation needs nuance. Goose has been growing, and its current docs list a Turso driver. So the answer is no longer &amp;ldquo;Goose can&amp;rsquo;t talk to Turso.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;But &amp;ldquo;there&amp;rsquo;s a driver&amp;rdquo; and &amp;ldquo;this is the smoothest operational path for my Turso migrations&amp;rdquo; are not the same claim. The friction I care about isn&amp;rsquo;t just opening a connection. It&amp;rsquo;s the whole lifecycle: creating the tracking table, applying safely, transaction behavior, the right remote protocol, auth, status, and a workflow I trust in a short-lived feature branch and in production. For that job I still prefer Geni for LibSQL/Turso, and Goose stays my default for Postgres/MySQL/MariaDB.&lt;/p&gt;
&lt;h2 id=&#34;when-to-use-something-heavier&#34;&gt;When to use something heavier&lt;/h2&gt;
&lt;p&gt;Lightweight SQL migration tools aren&amp;rsquo;t always enough. I&amp;rsquo;d start looking at Atlas, Liquibase, Flyway, Sqitch, or an ORM-native system when the problem changes shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You need schema drift detection across many environments.&lt;/li&gt;
&lt;li&gt;You want to declare desired schema and have the tool compute the migration.&lt;/li&gt;
&lt;li&gt;You need online migration planning for large tables.&lt;/li&gt;
&lt;li&gt;You operate hundreds or thousands of tenant databases.&lt;/li&gt;
&lt;li&gt;You need policy checks, approval workflows, or compliance artifacts before a migration can merge.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those are real needs. They&amp;rsquo;re also real costs. The heavier tool might be worth it, but I wouldn&amp;rsquo;t start there by default.&lt;/p&gt;
&lt;p&gt;For most projects, the better starting point is still: write a small SQL migration, review it in git, run &lt;code&gt;status&lt;/code&gt;, apply it, and keep the history boring.&lt;/p&gt;
&lt;h2 id=&#34;my-rule-of-thumb&#34;&gt;My rule of thumb&lt;/h2&gt;
&lt;p&gt;PostgreSQL, MySQL, or MariaDB, I reach for Goose. LibSQL/Turso, I reach for Geni. Not written in Go or Rust? I still consider both, because the CLI boundary matters more than the implementation language. Migrations are an operational concern before they&amp;rsquo;re an application concern.&lt;/p&gt;
&lt;p&gt;And if the story starts needing drift management, online planning, policy gates, or fleet-wide orchestration, I stop trying to stretch a lightweight tool and pick something heavier on purpose. For everything else, Geni and Goose land right in the sweet spot: small, SQL-first, scriptable, and capable enough to keep schema changes moving without turning migrations into a platform.&lt;/p&gt;
&lt;h2 id=&#34;sources-and-further-reading&#34;&gt;Sources and further reading&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Geni GitHub repository: &lt;a href=&#34;https://github.com/emilpriver/geni&#34;&gt;https://github.com/emilpriver/geni&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Turso: &amp;ldquo;Database migrations with Geni and libSQL&amp;rdquo;: &lt;a href=&#34;https://turso.tech/blog/database-migrations-with-geni&#34;&gt;https://turso.tech/blog/database-migrations-with-geni&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Goose documentation: &lt;a href=&#34;https://pressly.github.io/goose/&#34;&gt;https://pressly.github.io/goose/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Goose GitHub repository: &lt;a href=&#34;https://github.com/pressly/goose&#34;&gt;https://github.com/pressly/goose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Atlas guide on Turso connection URLs: &lt;a href=&#34;https://atlasgo.io/guides/sqlite/turso&#34;&gt;https://atlasgo.io/guides/sqlite/turso&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>