
From Next.js to Go: a migration guide for SaaS founders
How to migrate an existing Next.js SaaS to a Go stack with templ and HTMX. Step by step, what to port first, common gotchas, and how to avoid downtime.
Most “switch to Go” posts assume you are starting from a blank repo, which almost nobody actually is. The real situation looks more like this: you have a Next.js SaaS with paying customers, the hosting bill keeps climbing, every major release breaks something, and you want out.
This is a practical guide to migrating from Next.js to Go without losing customers or sleep along the way.
Should you migrate at all
Be honest about the reason.
Good reasons to migrate:
- Hosting cost is hurting you. Vercel bill is climbing into “this changes my runway” territory. (See the cost math.)
- Your team is small and Next.js complexity is eating your shipping speed.
- You are tired of breaking changes and constant upgrades.
- You are nervous about 36 months of npm supply chain incidents and want a smaller attack surface.
- You want to run on a $5 VPS instead of a $200 platform plan.
- You are bottlenecked by cold-starts on serverless and a long-running Go binary fixes it.
Bad reasons to migrate:
- A hot take on X told you Go is “faster” (sometimes it is, sometimes the bottleneck is your database).
- You watched a YouTube video. Watch ten more and a deploy first.
- Your CEO read about Go on Hacker News.
- You think TypeScript is “ugly.”
If you do not have a concrete pain you are trying to fix, do not migrate. A migration is months of work. Spend that time on customers.
The two migration patterns
There are two ways to migrate, and they have very different risk profiles.
Pattern A: big bang
Stop adding features to Next.js. Build the whole product in Go. Cut over in one weekend.
This is what most founders imagine. It is also a disaster waiting to happen. Anything you missed during the rewrite hits production at the worst possible time. Customers see bugs you have not seen yet.
Only do a big bang if your product is small, you have weeks of buffer in your runway, and your customers will be patient about a few weeks of bumps.
Pattern B: strangler fig
Run Next.js and Go side by side. Migrate one route at a time. Route at the edge.
This is the boring, professional way. It is also the way you will actually finish.
The pattern:
- Put both apps behind a reverse proxy (Caddy, NGINX, Cloudflare Workers).
- Default to Next.js for everything.
- Pick one route. Build the Go version. Add an exception in the proxy to send that route to Go.
- Verify, monitor, sleep on it.
- Pick the next route.
You ship a working migration in tiny, reversible steps. If a Go route is broken, flip the proxy back to Next.js and fix it without an outage.
This is how every successful migration to Go I have seen actually happened. Pattern B.
What to migrate first
Counterintuitively, do not start with the home page. Start with whatever route is the most stable and the least visited. You want to learn the new stack before you touch revenue-critical paths.
A typical order:
- Static pages. Legal pages, about, contact. They never change. They are easy to port. They give you reps with templ.
- The blog. Markdown-driven, low-traffic, easy to verify.
- The docs. Same engine as the blog. Bigger surface area, still no business logic.
- The landing page. Higher traffic but no auth. Watch your conversion rate, not your code.
- The marketing site as a whole. All non-authenticated routes.
- Sign up and login. Now you have to think about session compatibility.
- Settings and account pages. Read-mostly authenticated routes.
- The product itself. The thing your customers pay for. Last because it is highest risk.
By the time you get to step 7, you know the stack cold. By step 8, you have months of evidence the Go side is working.
Handling shared sessions
The trickiest part of a side-by-side migration is sessions. If a user logs into the Next.js app and then visits a Go route, they need to stay logged in.
You have two options.
Option 1: shared cookie, same secret
Both apps speak the same session cookie format. Both apps verify the same signing key. Both apps look up sessions in the same database.
In practice this means picking a session library or pattern on the Go side that matches what Next.js is doing. If Next.js uses signed cookies (NextAuth’s default), you implement the same signature scheme on the Go side.
It is fiddly. Test it before you cut any real traffic.
Option 2: shared session store
Both apps store sessions in the same Postgres table (or Redis). Each app reads the session by ID from the cookie and looks it up.
This is cleaner. It is also what I recommend. Sessions become just rows in a table.
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
user_id BIGINT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Both apps insert, both apps select, both apps delete. Migration done.
Handling the database
You almost certainly want to keep the same database. Postgres on Supabase, Neon, or your own host. Both apps talk to it.
You will hit two friction points.
ORM differences
Prisma generates a JavaScript client. Go uses sqlc or pgx or gorm or bun. They do not share migrations.
The fix: pick one tool as the migration authority. I recommend goose or tern on the Go side, with SQL files in a migrations/ folder. Both apps read those migrations. Prisma reads them via prisma db pull to update its schema.prisma. Or, drop Prisma migrations entirely once the Go side is the source of truth.
Type drift
If your Prisma schema and your Go structs ever disagree, you will ship a bug. Use sqlc to generate Go structs from your SQL schema, and prisma db pull to keep the JS schema in sync. Both halves stay aligned with the database, which is the source of truth.
Stripe and webhook routing
Your Next.js app currently receives Stripe webhooks at, say, /api/webhooks/stripe. You do not want to move the webhook URL because Stripe configuration is one of those “I will deal with that later” things.
Pattern: keep the webhook route on whichever app currently owns billing. When you eventually move billing to Go, change the Stripe webhook endpoint to point at the Go app. Stripe lets you do this with zero downtime if you keep both endpoints alive briefly.
If you are on Polar, same advice: change the URL in the Polar dashboard once the Go handler is ready.
SEO during migration
This is the part that genuinely keeps me up at night.
URL structure matters. If /blog/[slug] becomes /blog/{slug}, that is a non-issue because the URL is identical to a search engine. As long as the path stays the same, Google does not care which language renders it.
Things that do matter:
- Sitemap URL must keep working. Both apps can produce the sitemap, just pick one.
- Canonical tags must point to the same URLs you were using.
- Page titles, meta descriptions, OpenGraph tags must match (or improve).
- Server response time should not regress. Set up monitoring before you cut over.
- Internal links must keep working. Watch your 404 logs the week after each cut-over.
A good migration improves SEO because server-rendered HTML is faster than React hydration. I have seen products bump their Core Web Vitals scores meaningfully after a Go migration. Take advantage. Pre-render every page you can.
How long does it really take
For a typical mid-sized SaaS (say, 30 to 50 routes, auth, billing, settings, dashboard, blog, docs), a Pattern B migration takes a solo founder 3 to 6 months of part-time work or 1 to 2 months full time.
The first month feels slow because you are learning the stack. The last month feels slow because you are migrating high-risk routes. The middle is the productive part.
Resist the temptation to migrate everything at once. Resist the temptation to refactor while migrating. Port-as-is, optimize later.
What you give up
Be honest with yourself about the trade-offs.
- You lose the React component ecosystem. shadcn/ui has no direct Go equivalent (templUI is close, but the components are different).
- You lose hot module reload. templ has a watcher, but it is not as snappy as Vite’s HMR.
- You lose some of the AI tooling magic. Cursor and Claude Code are still excellent at Go, but the React feature gap is widening.
- You lose the recruiting pool. Go developers exist; React developers are everywhere.
If those trade-offs are deal-breakers, do not migrate. If they are acceptable, the Go side is genuinely a better daily experience.
A migration timeline I would actually follow
Here is a real plan you can copy.
Week 1. Set up the Go app side by side. Static routes only. Legal pages, about, contact. Get the deploy story working. Get monitoring working.
Week 2-3. Migrate the blog and docs. Both apps share the same markdown content folder. Cut traffic to Go for these routes.
Week 4-5. Migrate the landing page. Watch conversion rates carefully. A/B test if you can.
Week 6-8. Set up shared session storage. Migrate sign up, login, password reset to Go. Verify on staging with real cookies.
Week 9-12. Migrate settings and account pages. Migrate the simpler dashboard pages.
Week 13+. Migrate the core product, one major route per week, with a soak time between each.
When the last Next.js route is migrated, you delete the Next.js repo. The cleanup is a celebration.
The Go boilerplate option
You do not have to invent the Go side from scratch. goilerplate ships auth (magic-link, Google and GitHub OAuth, password reset), Polar and Stripe billing with signed webhooks (more providers on the roadmap), Resend email, blog, docs, SEO scaffolding, dark mode, S3 file uploads, and the full templ + HTMX + Tailwind stack. Start with it, point it at your existing database, and you skip months of foundation work. The foundation story is here if you want the background.
The migration is still real work, but you do it on top of code that already works in production, with a stack that I actually enjoy writing day to day. I ship into goilerplate every week. Your migration target keeps improving while you are migrating, and every change lands in your copy via lifetime updates.
If you are on Next.js and looking for the door, it is right there. The grass really is greener on the Go side once you get used to it. Just take it one route at a time.