Sign up
await authClient.signUp.email({
email,
password,
name,
username
});Internal implementation guide
Panduan migrasi aplikasi menuju `apps/api` sebagai pusat data, auth, validasi, dan komposisi lintas database. Dokumen ini dibuat untuk web, server components, client components, dan mobile Expo.
Semua aplikasi selain `apps/api` harus berhenti membaca database langsung secara bertahap. Aplikasi UI mengambil data melalui HTTP API, sementara `apps/api` menjadi boundary untuk validasi, auth, rate limit, CORS, dan komposisi data lintas database fisik.
| Domain | Client | Kepemilikan data |
|---|---|---|
| Identity | db.runner | users, sessions, organizations, workspaces, memberships, permissions |
| Catalog | db.catalog | products, categories, variants, media, storefront material |
| Commerce | db.commerce | customers, carts, orders, invoices, payments, shipments |
| Inventory | db.inventory | warehouses, stock, batches, movements, transfers |
Relasi lintas database disimpan sebagai scalar id seperti `organizationId`, `productId`, atau `memberId`. Jangan membuat aplikasi UI menggabungkan data lintas domain sendiri; buat endpoint komposisi di `apps/api`.
Gunakan nama env yang eksplisit per target runtime. Prefix publik hanya dipakai untuk URL aman yang boleh terlihat di browser atau mobile.
# apps/api DATABASE_IDENTITY_URL= DATABASE_CATALOG_URL= DATABASE_COMMERCE_URL= DATABASE_INVENTORY_URL= BETTER_AUTH_SECRET= BETTER_AUTH_URL=https://api.example.com CORS_ALLOWED_ORIGINS=https://app.example.com,https://landing.example.com AORI_API_PROXY_SECRET= # Next.js apps NEXT_PUBLIC_AORI_API_URL=https://api.example.com AORI_API_PROXY_SECRET= # Expo / React Native EXPO_PUBLIC_AORI_API_URL=https://api.example.com
`AORI_API_PROXY_SECRET` hanya boleh digunakan dari server runtime seperti Next.js route handler/server action. Jangan pernah kirim secret ini ke browser atau React Native.
Buat satu wrapper `apiFetch` per aplikasi. Wrapper harus menambahkan `Accept`, `Content-Type`, `x-request-id`, membaca JSON dengan aman, dan mengubah error API menjadi error yang konsisten di UI.
type ApiFetchOptions = Omit<RequestInit, "body"> & {
body?: unknown;
token?: string;
};
const API_URL = process.env.NEXT_PUBLIC_AORI_API_URL!;
export async function apiFetch<T>(path: string, options: ApiFetchOptions = {}) {
const { body, token, headers, ...init } = options;
const response = await fetch(new URL(path, API_URL), {
...init,
cache: "no-store",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"x-request-id": crypto.randomUUID(),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...headers
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const text = await response.text();
const data = text ? JSON.parse(text) : null;
if (!response.ok) {
throw new Error(data?.error?.message ?? "Request failed");
}
return data as T;
}Untuk server-to-server dari Next.js, teruskan user yang sudah divalidasi oleh better-auth dengan header proxy. Pola ini membantu migrasi karena `apps/app` bisa tetap menjadi session holder sementara `apps/api` mulai mengambil alih domain data.
await apiFetch("/api/organization", {
method: "POST",
headers: {
"x-aori-user-id": session.user.id,
"x-aori-proxy-secret": process.env.AORI_API_PROXY_SECRET!
},
body: { name, email, category: "RETAIL" }
});Target akhir yang disarankan adalah better-auth hidup di `apps/api`, lalu aplikasi lain memakai client better-auth ke origin API. Selama migrasi, `apps/app` boleh tetap memegang route `/api/auth/[...all]` dan meneruskan operasi domain ke `apps/api`.
await authClient.signUp.email({
email,
password,
name,
username
});await authClient.signIn.email({
email,
password
});await authClient.signOut();
Operasi user seperti update profile, delete account, change password, email OTP, two-factor, passkey, dan multi-session sebaiknya lewat endpoint better-auth lebih dulu. Setelah session valid, endpoint domain di `apps/api` hanya perlu memanggil `requireCurrentUser`.
// Contoh delete user melalui endpoint domain API, setelah session divalidasi di server caller.
await apiFetch(`/api/auth/users/${userId}`, {
method: "DELETE",
headers: {
"x-aori-user-id": session.user.id,
"x-aori-proxy-secret": process.env.AORI_API_PROXY_SECRET!
}
});Route publik boleh dibaca langsung dari client. Route mutasi harus memakai bearer/session atau proxy header dari server terpercaya.
| Group | Route |
|---|---|
| System | GET /api, GET /api/v1/health, GET /api/v1/meta |
| Auth | /api/auth/* through better-auth handler, bearer and session aware |
| Region | GET /api/62/provinces, /cities, /districts, /villages |
| Organization | /api/organization/:organizationId and nested product/category/faq routes |
| Future commerce | /api/client/store/:storeId/checkout and order status routes |
Format error standar adalah `{ "error": { "code": "...", "message": "...", "requestId": "..." } }`. UI tidak perlu membaca stack trace atau string error mentah.
Mobile tidak punya cookie browser yang sama seperti web. Gunakan bearer token atau storage session yang didukung better-auth client. Jangan mengirim `AORI_API_PROXY_SECRET` dari aplikasi mobile.
const API_URL = process.env.EXPO_PUBLIC_AORI_API_URL!;
export async function mobileApiFetch<T>(path: string, token?: string) {
const response = await fetch(`${API_URL}${path}`, {
headers: {
Accept: "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {})
}
});
const data = await response.json();
if (!response.ok) throw new Error(data?.error?.message ?? "Request failed");
return data as T;
}Untuk Expo, simpan token/session di secure storage, refresh saat app kembali aktif, dan selalu handle `401` dengan navigasi ke sign-in. Upload media dari mobile sebaiknya memakai signed upload endpoint di API, bukan credential Cloudinary langsung.
Pipeline minimum untuk `apps/api` dan integrasi client harus memastikan schema, tipe, lint, dan build selalu sinkron.
bun install --frozen-lockfilebun run db:generatebun run type-checkbun run lintbun run buildTambahkan smoke test setelah deploy: `GET /healthz`, `GET /api`, preflight CORS dari origin app, lalu satu route publik dan satu route protected dengan token test.