Skip to content

Authentication

Auth Flow Overview

Sistem auth menggunakan better-auth dengan Drizzle adapter, running di Cloudflare Workers. Ada 3 metode login dan 5 role. Semua user baru mulai dari role user dan wajib menyelesaikan onboarding sebelum mendapatkan role spesifik.

Stack

  • better-auth 1.6.9 (latest stable)
  • Drizzle adapter with PostgreSQL provider
  • Cloudflare Workers runtime

Login Methods

1. Google OAuth

User klik "Login dengan Google" → redirect ke Google consent → callback ke /api/auth/callback/google → session cookie di-set → redirect ke app.

Setup:

  • Google Client ID dan Client Secret disimpan sebagai Cloudflare secrets.
  • Redirect URI harus didaftarkan di Google Console: https://<domain>/api/auth/callback/google.
  • BETTER_AUTH_URL harus di-set ke domain aplikasi (e.g., https://xprivate.id) untuk menghindari redirect_uri_mismatch error.

Frontend:

ts
// apps/web/src/lib/auth.ts
import { createAuthClient } from "better-auth/svelte";

export const authClient = createAuthClient({
  fetchOptions: { credentials: "include" },
});
svelte
<script>
  import { authClient } from "$lib/auth";
  const session = authClient.useSession();
</script>

<button onclick={() => authClient.signIn.social({ provider: "google" })}>
  Masuk dengan Google
</button>

2. Email / Password

User input email + password → /api/auth/sign-in/email → better-auth verify dengan scrypt → session cookie di-set.

Security:

  • Password di-hash dengan scrypt via node:crypto.scrypt.
  • nodejs_compat flag diaktifkan di wrangler config.
  • Tidak pakai bcrypt atau argon2 (native bindings tidak work di Workers).

User input email → klik "Kirim Magic Link" → Plunk kirim email dengan link → user klik link → auto-login → session cookie di-set.

Setup:

  • Plunk API key disimpan sebagai Cloudflare secret.
  • Email template dikelola di @packages/email.

Role System

5 Roles

RoleDeskripsiDibuat oleh
userUnonboarded — belum mengisi profil wajibSelf-registration (default)
studentStudent — bisa create order, confirm sessionsOnboarding (pilih "Murid")
teacherTeacher — bisa confirm sessions, submit presenceOnboarding (pilih "Guru") + admin approval
adminBackoffice staff — full admin access kecuali inviteAuto-promote via XPRIVATE_ADMIN_EMAILS
ownerPlatform owner — full access + bisa invite adminsAuto-promote via XPRIVATE_OWNER_EMAILS

Onboarding Flow

Semua user baru register dengan role user. Untak bisa akses fitur platform, wajib:

  1. Isi Full Name
  2. Isi Phone Number (dengan country code)
  3. Pilih role: Murid (Student) atau Guru (Teacher)
  4. Isi KTP dan upload photo KTP (wajib untuk Teacher)
  5. Upload Profile Photo (wajib untuk Teacher)
  6. Isi KTP Details (optional)
  7. Isi Bio — Teacher wajib min 200 karakter
  8. Submit
  • Student → role langsung jadi student, redirect ke /orders
  • Teacher → insert ke teacher_applications (status pending), redirect ke /profile → menunggu admin approval

Role Rules

  • Mutual exclusion: admin dan owner tidak bisa jadi student atau teacher. Sebaliknya, student dan teacher tidak bisa jadi admin atau owner.
  • Owner invite: Hanya owner yang bisa invite admin baru via endpoint POST /api/v1/owner/invite-admin.
  • Default role: Registrasi baru default ke user.
  • Auto-promote: Email yang terdaftar di XPRIVATE_OWNER_EMAILS atau XPRIVATE_ADMIN_EMAILS akan auto-promote ke owner atau admin saat registrasi.
  • Teacher application: Teacher tidak langsung dapat role. Apply via onboarding → masuk teacher_applications table → admin approve/reject.
  • Role guards: Semua protected pages redirect user role ke /onboarding. Onboarding page redirect non-user ke halaman role mereka.

Middleware Chain

oRPC menggunakan middleware chain untuk progressive context enrichment:

ProcedureMiddleware ChainUse For
publicProcedurebasePublic routes (health, landing data)
authedProcedurebase.use(withPassiveAuth).use(requireAuth)Any logged-in user
studentProcedureauthedProcedure.use(requireStudent)Role = student
teacherProcedureauthedProcedure.use(requireTeacher)Role = teacher
adminProcedureauthedProcedure.use(requireAdminOrOwner)Role = admin atau owner
ownerProcedureauthedProcedure.use(requireOwner)Role = owner only

Session Strategy

  • better-auth menggunakan database sessions (Postgres-backed).
  • Session cookie name: xprivate-auth.session_token
  • Session duration: 7 hari (cookie maxAge dan DB expiresIn diset ke 604800 detik)
  • Session resolve di hooks.server.ts dan dipass ke $page.data.session via +layout.server.ts.

SSR Session Flow

Request → hooks.server.ts
              ├── /api/auth/* → better-auth handler
              └── Other routes → auth.api.getSession() → event.locals.session

                              +layout.server.ts → return { session }

                              $page.data.session available in all pages

Route Guards

Server-side role guards di +layout.server.ts:

ts
// routes/admin/+layout.server.ts
export const load = async ({ locals }) => {
  const session = locals.session;
  if (!session) throw redirect(302, "/");
  if (!["admin", "owner"].includes(session.user.role)) {
    throw redirect(302, "/profile");
  }
  return {};
};

Bearer Token (Non-Browser / API Testing)

Untuk non-browser clients dan OpenAPI testing, better-auth Bearer plugin mengizinkan autentikasi via Authorization: Bearer <token> header. Bearer token di-obtain dari response header set-auth-token setelah sign-in.

JWT Plugin

JWT plugin menyediakan:

  • Endpoint /api/auth/token — generate JWT token dari session aktif
  • Endpoint /api/auth/jwks — public key set untuk verifikasi JWT eksternal
  • Table jwks di Postgres untuk menyimpan key pair

JWT tidak menggantikan database session — digunakan untuk services yang memerlukan JWT atau testing dari OpenAPI client.

Flow:

  1. Sign-in via Google/Email/Magic Link → dapat session cookie
  2. Panggil GET /api/auth/token dengan session cookie → dapat JWT
  3. Gunakan Authorization: Bearer <jwt> untuk API calls selanjutnya

2FA (Post-MVP)

  • better-auth support 2FA via TOTP plugin.
  • Jangan implementasikan di MVP. Schema harus allow future addition tapi tidak ada UI atau API untuk itu dulu.

Env Variables

VariablePurpose
BETTER_AUTH_URLBase URL aplikasi (e.g., https://xprivate.id)
BETTER_AUTH_SECRETSecret untuk session signing
GOOGLE_CLIENT_IDGoogle OAuth client ID
GOOGLE_CLIENT_SECRETGoogle OAuth client secret
PLUNK_API_KEYPlunk API key untuk email
PLUNK_FROMFrom address untuk email (e.g. noreply@xprivate.id)

Decisions

Scrypt over bcrypt/argon2

bcrypt dan argon2 memerlukan native bindings yang tidak work di Cloudflare Workers. node:crypto.scrypt dengan nodejs_compat flag sudah verified working dan secure.

Database sessions sebagai default

Database sessions lebih aman untuk platform ini karena:

  • Session bisa di-revoke secara instan (penting untuk admin actions).
  • Tidak perlu handle JWT refresh complexity.
  • Neon Postgres provides edge-compatible connection pooling.

JWT hanya digunakan untuk specific use cases: non-browser clients, external service integration, dan OpenAPI testing.

Released under the MIT License.