Back to all posts
Published 2026-05-081 min read

Notes on Next.js 16 + next-intl + Server Actions

What I learned wiring up AlphaNode's i18n routing alongside Server Actions and Supabase in the new App Router.

nextjsengineering

A few snags I hit while putting together the AlphaNode stack. Recording them here mostly so I find them again in six months.

1. The [locale] segment owns the root layout

The Next.js docs say "every app needs a root layout." With next-intl's localePrefix: "always", the cleanest move is to delete app/layout.tsx entirely and put <html lang={locale}> inside app/[locale]/layout.tsx. The middleware guarantees every route falls under a locale segment, so there's no orphan request without a locale.

2. setRequestLocale is not optional for static rendering

Without it, getTranslations falls back to dynamic rendering, and you lose the SSG win on locale-prefixed routes. Call it at the top of every server component that uses translations:

const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations("Dashboard");

3. Server Actions and Supabase: use the service role on the server only

The anon key can read/write through RLS policies, but for a Server Action that creates a decision log entry on behalf of the user, the cleanest path is a server-only Supabase client using SUPABASE_SERVICE_ROLE_KEY. Make sure it's not prefixed with NEXT_PUBLIC_ so it never leaks to the browser bundle.

import { createClient } from "@supabase/supabase-js";

export const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!,
  { auth: { persistSession: false } }
);

4. revalidatePath with a locale-prefixed route

When you submit from /en/decision-log and want the table to refresh, you have two choices:

  • Call revalidatePath("/[locale]/decision-log", "page") — re-renders the segment across locales.
  • Call revalidatePath(\/${locale}/decision-log`)` — re-renders only this locale.

For MVP I picked the segment form. It costs a few extra renders but keeps the action body simple.

That's it. Real-world i18n routing isn't hard, just fiddly. Once it's wired up correctly, you stop thinking about it.