{
  "version": "https://jsonfeed.org/version/1",
  "title": "Libsql on LLBBL Blog",
  "icon": "https://avatars.micro.blog/avatars/2023/40/125738.jpg",
  "home_page_url": "https://llbbl.blog/",
  "feed_url": "https://llbbl.blog/feed.json",
  "items": [
      {
        "id": "http://llbbl.micro.blog/2026/07/08/geni-vs-goose-for-lightweight.html",
        "title": "Geni vs. Goose for lightweight database migrations",
        "content_html": "<p>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&rsquo;t want much more standing in the way.</p>\n<p>That&rsquo;s why I like both Geni and Goose.</p>\n<p>Neither one is trying to be a full application framework. They don&rsquo;t require the rest of the app to be written in the same language as the migration tool. They&rsquo;re small enough to understand, script, and run from CI, but they have enough features that they don&rsquo;t feel like a pile of shell scripts once a project grows real production databases.</p>\n<p>The catch is that they&rsquo;re good at slightly different jobs. I&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&rsquo;d reach for the same shape again, even on projects that aren&rsquo;t written in Go or Rust.</p>\n<h2 id=\"the-short-version\">The short version</h2>\n<p>Use <strong>Goose</strong> 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.</p>\n<p>Use <strong>Geni</strong> when LibSQL or Turso is part of the system. It&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 &ldquo;SQLite-like, but remote and edge-hosted&rdquo; starts to expose the rough edges of tools designed around local SQLite files.</p>\n<p>Reach for something heavier when migrations stop being ordered SQL files and turn into a schema-management problem. More on that at the end.</p>\n<h2 id=\"why-i-like-both\">Why I like both</h2>\n<p>The best thing about Geni and Goose is that the migration stays close to the database.</p>\n<p>There&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&rsquo;s exactly the right amount of machinery.</p>\n<p>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&rsquo;t need to be imported by the app.</p>\n<p>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.</p>\n<h2 id=\"where-goose-shines\">Where Goose shines</h2>\n<p>Goose is the boring choice, in the best sense. Its migration format is easy to read:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-sql\" data-lang=\"sql\"><span style=\"color:#75715e\">-- +goose Up\n</span><span style=\"color:#75715e\">-- +goose StatementBegin\n</span><span style=\"color:#75715e\"></span><span style=\"color:#66d9ef\">CREATE</span> <span style=\"color:#66d9ef\">TABLE</span> users (\n  id BIGSERIAL <span style=\"color:#66d9ef\">PRIMARY</span> <span style=\"color:#66d9ef\">KEY</span>,\n  email TEXT <span style=\"color:#66d9ef\">NOT</span> <span style=\"color:#66d9ef\">NULL</span> <span style=\"color:#66d9ef\">UNIQUE</span>\n);\n<span style=\"color:#75715e\">-- +goose StatementEnd\n</span><span style=\"color:#75715e\"></span>\n<span style=\"color:#75715e\">-- +goose Down\n</span><span style=\"color:#75715e\">-- +goose StatementBegin\n</span><span style=\"color:#75715e\"></span><span style=\"color:#66d9ef\">DROP</span> <span style=\"color:#66d9ef\">TABLE</span> users;\n<span style=\"color:#75715e\">-- +goose StatementEnd\n</span></code></pre></div><p>The forward and rollback changes live in one file, and a review comment can talk about both in one place.</p>\n<p>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:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-bash\" data-lang=\"bash\">goose -dir migrations/postgres/app/schema postgres <span style=\"color:#e6db74\">&#34;</span>$POSTGRES_APP_DSN<span style=\"color:#e6db74\">&#34;</span> up\ngoose -dir migrations/mysql/app/schema mysql <span style=\"color:#e6db74\">&#34;</span>$MYSQL_APP_DSN<span style=\"color:#e6db74\">&#34;</span> up\n</code></pre></div><p>For Go services there&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&rsquo;s clearer in code. Goose supports that without forcing every migration to become code.</p>\n<p>I reach for Goose first on Postgres/MySQL/MariaDB, on teams that already know SQL migrations, and in deploy scripts that just want <code>status</code>, <code>up</code>, <code>down</code>, and <code>version</code>.</p>\n<h2 id=\"where-geni-shines\">Where Geni shines</h2>\n<p>Geni has a similar direct feel, but its file model is different:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-text\" data-lang=\"text\">migrations/\n  1782054288_create_users_table.up.sql\n  1782054288_create_users_table.down.sql\n</code></pre></div><p>Forward migration in one file, rollback in another, both plain SQL. Geni can create the pair for you:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-bash\" data-lang=\"bash\">geni new create_users_table\ngeni status\ngeni up\n</code></pre></div><p>The more important point is LibSQL/Turso. Turso is not just &ldquo;a local SQLite file with a different name.&rdquo; LibSQL is SQLite-compatible, but a remote Turso database brings its own connection shape, auth token, protocol, and operational behavior. That&rsquo;s exactly where a migration tool either feels native or feels like you&rsquo;re pushing it through a small opening.</p>\n<p>Geni expects the Turso shape. The documented flow uses <code>DATABASE_URL</code> and <code>DATABASE_TOKEN</code>, it speaks LibSQL, and it can dump a <code>schema.sql</code> snapshot after migrations, which is handy as a review artifact and source-control reference.</p>\n<p>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 <code>DATABASE_URL</code> and <code>DATABASE_TOKEN</code>, points Geni at the right per-database folder, and masks tokens in debug output. That keeps the human command simple:</p>\n<div class=\"highlight\"><pre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"><code class=\"language-bash\" data-lang=\"bash\">mise run geni dev app status\nmise run geni prod app up\n</code></pre></div><p>One folder per logical database, paired up/down files, secrets outside the repo, a status check before deploy, and a confirmation prompt before production <code>up</code> or <code>down</code>.</p>\n<h2 id=\"about-goose-and-turso\">About Goose and Turso</h2>\n<p>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 &ldquo;Goose can&rsquo;t talk to Turso.&rdquo;</p>\n<p>But &ldquo;there&rsquo;s a driver&rdquo; and &ldquo;this is the smoothest operational path for my Turso migrations&rdquo; are not the same claim. The friction I care about isn&rsquo;t just opening a connection. It&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.</p>\n<h2 id=\"when-to-use-something-heavier\">When to use something heavier</h2>\n<p>Lightweight SQL migration tools aren&rsquo;t always enough. I&rsquo;d start looking at Atlas, Liquibase, Flyway, Sqitch, or an ORM-native system when the problem changes shape:</p>\n<ul>\n<li>You need schema drift detection across many environments.</li>\n<li>You want to declare desired schema and have the tool compute the migration.</li>\n<li>You need online migration planning for large tables.</li>\n<li>You operate hundreds or thousands of tenant databases.</li>\n<li>You need policy checks, approval workflows, or compliance artifacts before a migration can merge.</li>\n</ul>\n<p>Those are real needs. They&rsquo;re also real costs. The heavier tool might be worth it, but I wouldn&rsquo;t start there by default.</p>\n<p>For most projects, the better starting point is still: write a small SQL migration, review it in git, run <code>status</code>, apply it, and keep the history boring.</p>\n<h2 id=\"my-rule-of-thumb\">My rule of thumb</h2>\n<p>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&rsquo;re an application concern.</p>\n<p>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.</p>\n<h2 id=\"sources-and-further-reading\">Sources and further reading</h2>\n<ul>\n<li>Geni GitHub repository: <a href=\"https://github.com/emilpriver/geni\">https://github.com/emilpriver/geni</a></li>\n<li>Turso: &ldquo;Database migrations with Geni and libSQL&rdquo;: <a href=\"https://turso.tech/blog/database-migrations-with-geni\">https://turso.tech/blog/database-migrations-with-geni</a></li>\n<li>Goose documentation: <a href=\"https://pressly.github.io/goose/\">https://pressly.github.io/goose/</a></li>\n<li>Goose GitHub repository: <a href=\"https://github.com/pressly/goose\">https://github.com/pressly/goose</a></li>\n<li>Atlas guide on Turso connection URLs: <a href=\"https://atlasgo.io/guides/sqlite/turso\">https://atlasgo.io/guides/sqlite/turso</a></li>\n</ul>\n<blockquote>\n<p>I&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 <a href=\"https://micro.blog/llbbl?remote_follow=1\">@logan@llbbl.blog</a>.</p>\n</blockquote>\n",
        "date_published": "2026-07-08T10:00:00-05:00",
        "url": "https://llbbl.blog/2026/07/08/geni-vs-goose-for-lightweight.html",
        "tags": ["DevOps","Database migrations","Geni","Goose","Turso","Libsql","Sql"]
      }
  ]
}
