Appearance
Authentication
Auth system kita pakai better-auth 1.6 dengan Drizzle adapter untuk Cloudflare D1. Setup-nya pure di Cloudflare Workers tanpa backend tambahan.
Auth Methods
Google OAuth
User bisa login/register pakai Google account. Client ID dan client secret diambil dari Worker env bindings:
ts
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
}Email / Password
User bisa signup dengan email dan password. Password di-hash pakai scrypt via node:crypto.scrypt:
ts
async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(16).toString("hex");
const derivedKey = await scryptAsync(password, salt, 64);
return `${salt}:${derivedKey.toString("hex")}`;
}Verifikasi pakai timingSafeEqual untuk constant-time comparison.
Role System
3 Role Sederhana
| Role | Akses |
|---|---|
member | Data sendiri doang |
admin | Bisa akses data user lain untuk moderation |
owner | Sama kayak admin |
Config
Role di-define via additionalFields di better-auth config:
ts
additionalFields: {
role: {
type: "string",
defaultValue: "member",
required: false,
},
}Middleware Check
Role check di oRPC middleware — simple string comparison:
ts
if (user.role !== "admin" && user.role !== "owner") {
throw new ORPCError("FORBIDDEN");
}Tidak ada ACL system atau permission matrix. Kalau suatu saat butuh lebih granular, baru refactor.
Session Management
Cookie-Based Sessions
better-auth pakai cookie-based sessions secara default. Session token disimpan di browser, di-validate setiap request.
Session Flow
User login → better-auth create session → set cookie → subsequent requests carry cookie → middleware validate sessionGet Session di API
ts
const session = await authInstance.api.getSession({
headers: request.headers,
});Cross-Subdomain Cookie
Web frontend (soulmapatlas.app) dan API (api.soulmapatlas.app) share session cookie via cookie domain config:
ts
cookie: {
secure: true,
sameSite: "lax",
domain: env.BETTER_AUTH_COOKIE_DOMAIN, // e.g., "soulmapatlas.app"
}Auth Routes
Semua auth routes di-handle oleh better-auth handler di apps/api/src/index.ts:
ts
if (url.pathname.startsWith("/api/auth/")) {
return authInstance.handler(request);
}Endpoint yang tersedia otomatis:
POST /api/auth/sign-in/email— login dengan emailPOST /api/auth/sign-up/email— register dengan emailGET /api/auth/sign-in/google— initiate Google OAuthGET /api/auth/callback/google— Google OAuth callbackGET /api/auth/get-session— get current sessionPOST /api/auth/sign-out— logout
Drizzle Adapter
Import Path (better-auth 1.6)
ts
// ✅ Benar
import { drizzleAdapter } from "better-auth/adapters/drizzle";
// ❌ Salah
import { drizzleAdapter } from "better-auth/adapters/drizzle-adapter";
import { drizzleAdapter } from "@better-auth/drizzle-adapter";Schema
Auth schema (user, session, account, verification) ada di packages/db/src/schema/auth.ts. Drizzle adapter akan otomatis create/migrate tabel-tabel ini.
Database Provider
ts
database: drizzleAdapter(db, {
provider: "sqlite",
schema,
}),baseURL Requirement
better-auth 1.6 butuh baseURL di config. Kalau tidak ada, muncul warning dan OAuth callback bisa gagal:
ts
baseURL: env.BETTER_AUTH_URL || "http://localhost:8787",Secret dan Trusted Origins
Secret
secret wajib di-set untuk session encryption. Kalau tidak di-set, better-auth auto-generate dan session bakal invalidate tiap deploy:
ts
secret: env.BETTER_AUTH_SECRET,Trusted Origins
Untuk security, better-auth 1.6 butuh whitelist origin yang boleh hit auth endpoints:
ts
trustedOrigins: ["https://soulmapatlas.app", "https://api.soulmapatlas.app"],Bisa di-set via env BETTER_AUTH_TRUSTED_ORIGIN (comma-separated).
Environment Variables
| Variable | Scope | Description |
|---|---|---|
BETTER_AUTH_SECRET | Secret | Session encryption key. Wajib di production. |
BETTER_AUTH_URL | Var | Base URL API. Dev: http://localhost:8787, Prod: https://api.soulmapatlas.app |
BETTER_AUTH_COOKIE_DOMAIN | Var | Cookie domain untuk cross-subdomain. Prod: soulmapatlas.app |
BETTER_AUTH_TRUSTED_ORIGIN | Var | Comma-separated trusted origins. |
GOOGLE_CLIENT_ID | Var | Google OAuth client ID. |
GOOGLE_CLIENT_SECRET | Secret | Google OAuth client secret. |
Local Development
Copy template secrets:
bash
cp apps/api/.dev.vars.example apps/api/.dev.vars
# Edit apps/api/.dev.vars dengan value yang sesuai.dev.vars sudah di-.gitignore — tidak akan ter-commit.
Production
Set secrets via Wrangler CLI:
bash
wrangler secret put BETTER_AUTH_SECRET --env production
wrangler secret put GOOGLE_CLIENT_SECRET --env productionVars non-sensitive sudah di-config di wrangler.jsonc per environment.
Security Notes
- Password selalu di-hash — tidak pernah store plain text.
- Session cookie
HttpOnlydanSecure(di production). - CORS header di-set manual di fetch handler (tidak ada CORS plugin di oRPC 1.14).
- OAuth redirect URL wajib didaftarkan di Google Console.