Authentication for Bangkok Business Apps with Next.js and Supabase: Step by Step
29 April 2026 · by Yunmin Shin
Why Use Supabase for Authentication?
Most Bangkok business web apps end up needing more than one kind of login: a clinic needs staff to see the booking calendar but not patient medical notes, a restaurant owner needs to see today's LINE orders without exposing that dashboard to the public internet, and a multi-location clinic group needs a manager at one branch to not see another branch's bookings. Rolling that by hand means managing password hashing, session tokens, and permission checks scattered across the codebase — each one a place to get it wrong.
Supabase Auth handles the identity side and, because it's built on Postgres, plugs directly into Row-Level Security policies. That means "can this user see this booking" becomes a database-level rule instead of a check you have to remember to add in every route handler that touches the bookings table.
How Do You Set Up Supabase Auth in Next.js?
npm install @supabase/supabase-js @supabase/ssr
Create two client utilities: a server client (utils/supabase/server.ts) that reads the session via cookies() for use in server components, middleware, and route handlers; and a browser client (utils/supabase/client.ts) that persists the session client-side for use in client components.
Add the Supabase URL and anon key to .env.local. The anon key is meant to be public — the actual security boundary is your RLS policies, not the key.
How Do You Model Roles for a Clinic or Restaurant Dashboard?
A typical setup has three or four roles: owner, staff, customer, and sometimes admin across multiple locations. Store the role on a profiles table linked to auth.users, then write RLS policies against it:
create policy "staff can view their clinic's bookings"
on bookings for select
using (
clinic_id in (
select clinic_id from profiles where profiles.id = auth.uid()
)
);
This is the piece that actually matters for a multi-location business: the database itself refuses to return another branch's bookings, regardless of what the application code does or forgets to check. If a bug ever lets a query run without the intended filter, RLS is the backstop.
How Do You Implement Email and Password Auth?
Use Supabase's signUp, signInWithPassword, and signOut inside Server Actions:
"use server";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export async function login(formData: FormData) {
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (error) redirect("/login?error=invalid_credentials");
redirect("/dashboard");
}
For a staff-facing dashboard this is often enough on its own — clinic and restaurant staff are a small, known group, and email/password with a confirmation email is simple to support. Supabase handles the confirmation email flow out of the box.
How Do You Add Google OAuth for Customers?
For a customer-facing side — say, a booking history page where a returning patient can see their past appointments — password friction costs conversions. Google OAuth removes the "create yet another password" step:
await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: `${origin}/auth/callback` },
});
Create a /auth/callback Route Handler that exchanges the code for a session with supabase.auth.exchangeCodeForSession(code).
A note on LINE: it's the default identity Thai users think in, but Supabase doesn't ship a built-in LINE provider the way it does Google. If a project genuinely needs "log in with LINE," that means wiring LINE Login as a custom OIDC-compliant provider rather than a one-line toggle — worth planning for up front rather than assuming it's equivalent effort to Google. In practice, most clinic and restaurant dashboards get more value from Google OAuth for customers and simple email/password for staff than from chasing LINE Login on day one.
How Do You Handle Password Resets and Session Expiry?
A password reset flow is easy to skip during initial build and then urgently needed the first week a clinic manager locks themselves out before a morning shift. Supabase covers this with resetPasswordForEmail:
await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/reset-password`,
});
The linked page collects a new password and calls supabase.auth.updateUser({ password }) once the reset session is active. Test this flow end to end before launch — a broken reset link is invisible until the one day someone actually needs it, and for a small clinic team without a dedicated IT contact, that usually means a call to whoever built the site.
For session length, Supabase's default access token lifetime (with automatic refresh via @supabase/ssr) is reasonable for most staff dashboards. For anything handling patient data specifically, consider a shorter session and a re-authentication prompt for sensitive actions, like exporting a patient list — convenience and PDPA-conscious handling of personal data are a real tradeoff here, not just a checkbox.
How Do You Protect Routes?
Use Middleware to check for a valid session before a protected page renders:
// middleware.ts
const { data: { user } } = await supabase.auth.getUser();
if (!user) return NextResponse.redirect(new URL("/login", request.url));
This runs on the Edge before the page is served, so an unauthenticated staff member hitting /dashboard directly never sees a flash of another clinic's booking data before being redirected.
Always use getUser(), not getSession(), in server-side code. getUser() re-validates the session against Supabase's servers on every call; getSession() only reads the cookie without checking it's still valid, which matters if a staff member's account gets deactivated mid-session — getUser() catches that on the next request, getSession() won't.
Ready to Build Something Fast?
Get a free quote. We reply within 24 hours.
Ready to build something fast and scalable?
Get a free project quote. We reply within 24 hours.
Get a Free Quote →