🟡 Hiển thị nội dung LumiBase trên app Next.js
Xây dựng trang Next.js đầu tiên với nội dung được phân phối từ LumiBase.
[!NOTE] Tutorial này cho phiên bản nào? Đúng từ LumiBase
0.9.0trở lên (verify gần nhất trên1.0.0-rc.1). Vẫn đúng cho các bản mới hơn cho tới khi một trong các API contract ở bảng Tương thích thay đổi — xem bảng đó để chọn đúng version, mới nhất ở trên cùng.
Bạn sẽ:
- Chạy LumiBase ở local (CMS API + Studio).
- Hoàn tất setup wizard và tạo collection
postsvới vài item đã publish. - Tạo API key dài hạn và xác nhận
siteId. - Dựng app Next.js nhỏ để đọc các bài viết đó bằng package
lumibasechính thức (fetchthuần được trình bày sau như một lựa chọn thay thế).
Kết thúc, bạn có trang http://localhost:3000 liệt kê các bài viết nằm trong LumiBase.
Cần có: Node.js ≥ 22, pnpm ≥ 9, Docker + Docker Compose, Git.
How the pieces fit together
|
🖥️
App Next.jslocalhost:3000frontend (code của bạn) |
GET /api/v1/items/posts
➡️
Authorization: Bearer <token>
X-Lumi-Site: <siteId> ⬅️
{ "data": [ …posts ] }
|
🟡
LumiBaselocalhost:1989 · APIlocalhost:2026 · StudioPostgres · Redis · … |
LumiBase là backend headless (API + Studio quản trị). Next.js chỉ là client gọi vào
Delivery API qua HTTP. Mỗi request mang theo hai thứ: một bearer token (bạn là ai)
và header X-Lumi-Site (đọc từ tenant/site nào).
Step 1 — Run LumiBase locally
git clone https://github.com/khuepm/lumibase.git
cd lumibase
pnpm install
# Backing services: PostgreSQL, Redis, MeiliSearch, Logto
docker compose -f docker/docker-compose.yml up -d
# Database migrations
pnpm db:migrate
# Start CMS API (:1989) + Studio (:2026)
pnpm dev
Khi pnpm dev đang chạy:
| Dịch vụ | URL | Là gì |
|---|---|---|
| 🔌 CMS API | http://localhost:1989 | REST API mà app Next.js gọi vào |
| 🎛️ Studio | http://localhost:2026 | UI quản trị để model & soạn nội dung |
Xem Local Development để biết danh sách dịch vụ đầy đủ và cách xử lý sự cố.
Step 2 — Complete the setup wizard
Lần chạy đầu database trống nên CMS kích hoạt setup wizard. Mở
http://localhost:1989/setup và:
- Tạo admin user đầu tiên (email + mật khẩu — nhớ kỹ).
- Đặt tên site và ngôn ngữ mặc định.
- Hoàn tất. Response có kèm backup codes một lần — cất nơi an toàn.
[!IMPORTANT] Setup wizard tạo một site mặc định với id
__default__. Đó làsiteIdcho toàn bộ phần dưới. (Xác nhận bất kỳ lúc nào bằngGET /api/v1/site— xem Bước 4.)
Kiểm tra setup đã xong:
curl http://localhost:1989/health
# → { ... "setup_complete": true }
Step 3 — Create a posts collection and add content
Trong Studio (http://localhost:2026):
| # | Thao tác |
|---|---|
| 1 | Vào Collections → New Collection, đặt tên là posts. |
| 2 | Thêm các trường: title (String), body (Text). Không thêm trường status — mỗi item đã có sẵn cột status dựng sẵn do luồng publish điều khiển. |
| 3 | Lưu collection. |
| 4 | Vào Content → posts → New Item. Tạo 2–3 mục và đặt status = published. |
Muốn dùng API? Tạo collection bằng
POST /api/v1/collectionsvà item bằngPOST /api/v1/items/posts. Xem API spec.
Step 4 — Get an API key and confirm your siteId
App Next.js của bạn xác thực bằng bearer token. Với bản triển khai thực tế bạn sẽ cần API key dài hạn, không phải token đăng nhập ngắn hạn.
4a. Đăng nhập để lấy token phiên làm việc (chỉ dùng để tạo API key):
curl -X POST http://localhost:1989/api/v1/auth/login \
-H "Content-Type: application/json" \
-H "X-Lumi-Site: __default__" \
-d '{ "email": "[email protected]", "password": "your-password" }'
Response (chú ý: trường này tên là token, đơn lẻ — phiên bản này không có
access_token/refresh_token riêng):
{
"data": {
"token": "eyJhbGciOi...",
"user": { "id": "usr_...", "email": "[email protected]" }
}
}
4b. Tạo API key dài hạn với token phiên làm việc đó:
curl -X POST http://localhost:1989/api/v1/api-keys \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token-from-4a>" \
-H "X-Lumi-Site: __default__" \
-d '{ "name": "nextjs-frontend" }'
Response — sao chép token (bắt đầu bằng lbk_ và chỉ hiển thị một lần duy nhất):
{
"data": {
"id": "...",
"name": "nextjs-frontend",
"prefix": "lbk_",
"token": "lbk_live_xxxxxxxxxxxxxxxx"
}
}
4c. Xác nhận siteId (kiểm tra cho chắc):
curl http://localhost:1989/api/v1/site \
-H "Authorization: Bearer lbk_live_xxxxxxxxxxxxxxxx" \
-H "X-Lumi-Site: __default__"
# → { "data": { "id": "__default__", "name": "My Site", ... } }
[!TIP] Giữ key
lbk_…chỉ ở phía server — không bao giờ gửi xuống browser. Bên dưới ta dùng nó từ Next.js Server Component nên nó không rời khỏi server.
4d. Cấp quyền tối thiểu cho key. Key mới tạo chưa có quyền nào. Thay vì gắn
role Administrator, hãy tạo một policy chỉ đọc posts, giới hạn ở item đã
publish, rồi gắn qua một role:
# Policy với đúng một quy tắc: "đọc các bài đã publish"
curl -X POST http://localhost:1989/api/v1/policies \
-H "Content-Type: application/json" -H "Authorization: Bearer <token-from-4a>" \
-H "X-Lumi-Site: __default__" \
-d '{ "name": "Blog read-only" }'
curl -X POST http://localhost:1989/api/v1/policies/<policy-id>/permissions \
-H "Content-Type: application/json" -H "Authorization: Bearer <token-from-4a>" \
-H "X-Lumi-Site: __default__" \
-d '{ "collection": "posts", "action": "read",
"permissions": { "status": { "_eq": "published" } } }'
# Role mang policy đó, rồi gắn role vào key
curl -X POST http://localhost:1989/api/v1/roles \
-H "Content-Type: application/json" -H "Authorization: Bearer <token-from-4a>" \
-H "X-Lumi-Site: __default__" -d '{ "name": "Blog Reader" }'
curl -X POST http://localhost:1989/api/v1/roles/<role-id>/policies \
-H "Content-Type: application/json" -H "Authorization: Bearer <token-from-4a>" \
-H "X-Lumi-Site: __default__" -d '{ "policyId": "<policy-id>" }'
curl -X POST http://localhost:1989/api/v1/api-keys/<key-id>/roles \
-H "Content-Type: application/json" -H "Authorization: Bearer <token-from-4a>" \
-H "X-Lumi-Site: __default__" -d '{ "roleId": "<role-id>" }'
Quy tắc được áp dụng ở phía server: request từ key này dù bỏ qua
status=published, hay hỏi thẳng id của một bản nháp, vẫn chỉ nhận về item đã
publish (404 với bản nháp). Thao tác ghi trả về 403.
Step 5 — Create the Next.js app
Trong một thư mục riêng (ngoài repo LumiBase):
npx create-next-app@latest my-lumibase-frontend
cd my-lumibase-frontend
Chọn mặc định (App Router, TypeScript). Tạo .env.local:
# .env.local — server-side only, NOT prefixed with NEXT_PUBLIC_
LUMIBASE_API_URL=http://localhost:1989
LUMIBASE_SITE_ID=__default__
LUMIBASE_TOKEN=lbk_live_xxxxxxxxxxxxxxxx
Ta gọi LumiBase từ một Server Component, nên token nằm lại phía server và không phải cấu hình CORS. Đây cũng là cách khuyến nghị cho production.
Step 6 — Fetch with the SDK
Cài lumibase. Một package cung cấp cả client bạn import lúc chạy lẫn CLI
lumibase dùng ở Step 7:
npm install lumibase
Tạo client một lần. Nó chỉ được import từ Server Component nên token không bao giờ xuống tới browser:
// lib/lumibase.ts
import { createLumiClient, legacyRest, type ItemRow } from 'lumibase'
// Các trường nội dung của collection, đúng như khai báo trong Studio.
export interface PostFields {
title: string
body: string
[key: string]: unknown
}
export type Post = ItemRow<PostFields>
export const lumibase = createLumiClient<{ posts: PostFields }>({
url: process.env.LUMIBASE_API_URL!,
siteId: process.env.LUMIBASE_SITE_ID!,
// API key tĩnh — bỏ qua luồng login
token: process.env.LUMIBASE_TOKEN!,
}).with(legacyRest())
// app/page.tsx
import { lumibase, type Post } from '@/lib/lumibase'
export default async function Home() {
// `status` là tham số riêng của list, không phải filter. Sắp xếp dùng tên
// snake_case của cột cấu trúc.
const { data: posts } = await lumibase.items('posts').list({
status: 'published',
sort: ['-created_at'],
limit: 20,
})
return (
<main style={{ maxWidth: 640, margin: '2rem auto', fontFamily: 'system-ui' }}>
<h1>Posts from LumiBase</h1>
{posts.length === 0 && <p>No published posts yet.</p>}
<ul>
{posts.map((post: Post) => (
<li key={post.id} style={{ marginBottom: '1.5rem' }}>
<h2>{post.data.title}</h2>
<p>{post.data.body}</p>
</li>
))}
</ul>
</main>
)
}
[!IMPORTANT] Trường nội dung nằm trong
.data. Mỗi dòng là mộtItemRow: các cột cấu trúc (id,status,createdAt, …) nằm ở cấp ngoài cùng, còn các trường bạn khai báo thì lồng bên trong —post.data.title, không phảipost.title.
Chạy thử:
# mở http://localhost:3000
npm run dev
Bạn sẽ thấy các bài đã publish. Giao diện render đại khái như sau:
Posts from LumiBaseHello, Edge 👋
My first post served from LumiBase.
Why a Content OS
Intent in, reconciled content out.
|
Đọc một bài đơn lẻ cũng tương tự. Mọi phản hồi khác 2xx đều ném LumiError kèm
status, ánh xạ gọn sang notFound():
// app/posts/[id]/page.tsx
import { notFound } from 'next/navigation'
import { LumiError } from 'lumibase'
import { lumibase, type Post } from '@/lib/lumibase'
// Bắt buộc. Thiếu dòng này thì route bị cache vĩnh viễn và nội dung sửa trong
// Studio không bao giờ hiện ra.
export const revalidate = 60
export default async function PostPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
let post: Post
try {
const res = await lumibase.items('posts').detail(id)
post = res.data
} catch (err) {
if (err instanceof LumiError && err.status === 404) return notFound()
throw err
}
return (
<article>
<h1>{post.data.title}</h1>
<p>{post.data.body}</p>
</article>
)
}
Một bản nháp mà credential không được phép thấy sẽ trả 404 — cùng đường đi với
một id không tồn tại — nên một bản nháp chưa từng publish không có đường nào vào
các trang này.
[!IMPORTANT] Khai báo
revalidatecho mọi route được cache, không chỉ trang danh sách. Route cógenerateStaticParamsnhưng thiếurevalidatechỉ được render một lần lúc build rồi cache vĩnh viễn, nên nội dung sửa trong Studio không bao giờ hiện ra. Bài publish sau khi build vẫn phục vụ được:dynamicParamsmặc định làtruenên Next render on-demand ở lần truy cập đầu tiên.
[!WARNING] Unpublish không giống với chưa từng publish, và cấu hình này KHÔNG bảo đảm thu hồi cứng. Một bản nháp chưa từng lên sóng thì không thể xuất hiện — API trả
404và chưa có trang nào được sinh ra. Nhưng một bài đã lên sóng rồi bị unpublish vẫn đọc được từ cache, vì ISR phục vụ HTML cũ rồi mới revalidate ở nền. Một phép đo, trên CMS thật vớirevalidate = 60: unpublish một bài mà trang của nó vừa được sinh lại — API ngừng ngay lập tức việc trả bài đó cho credential đọc, trong khi trang chi tiết và trang danh sách đều còn phục vụ nó thêm 64 giây. Sửa một bài đã quá cửa sổ thì hiện ra sau 3–4 giây, và một bài publish sau khi build thì truy cập được ở URL của nó ngay lập tức (trang danh sách mất ~60 giây để có nó). Hãy đọcrevalidatelà khoảng thời gian tối thiểu giữa hai lần một trang đi tìm dữ liệu mới, KHÔNG phải giới hạn trên cho việc phục vụ nội dung cũ. Ba điều làm cửa sổ dài hơn con số đó, và không cái nào là lỗi: request đầu tiên sau khi hết hạn theo thiết kế vẫn được trả từ cache cũ, một trang không ai truy cập thì không bao giờ được revalidate, và một lần sinh lại thất bại sẽ giữ nguyên HTML trước đó. Con số 64 giây ở trên là một đường đi — có traffic, sinh lại thành công — không phải mức trần. Vậy nếu nội dung của bạn có yêu cầu takedown, đừng dựa vào mặc định này. Hãy hạrevalidate, dùngcache: 'no-store'cho những route không được phép phục vụ nội dung đã thu hồi, hoặc kích on-demand revalidation từ một webhook của LumiBase khi item rời trạng tháipublished— đó là lựa chọn duy nhất ở đây hành động theo thay đổi thay vì chờ đồng hồ.
Đang phụ thuộc
@lumibase/sdk? Nó export đúng cùng một client —lumibasechỉ re-export lại để một cái tên bao trọn cả client lẫn CLI. Đổifrom 'lumibase'thànhfrom '@lumibase/sdk'là mọi thứ ở trên chạy nguyên vẹn.
Step 7 — Generate types in CI
lumibase types biến schema đang chạy thành định nghĩa TypeScript. Hãy commit
kết quả và để CI báo lỗi khi nó lệch với schema.
Tạo lumibase.config.json cạnh package.json (file này để commit — nó không
chứa secret):
{
"url": "http://localhost:1989",
"siteId": "__default__",
"typegen": { "out": "src/lumibase-types.d.ts" }
}
# ghi src/lumibase-types.d.ts — hãy commit
npx lumibase types
# thoát khác 0 nếu file đã cũ
npx lumibase types --check
# xem cấu hình đã resolve và kiểm tra kết nối
npx lumibase doctor
[!IMPORTANT] Typegen cần token của staff user, không phải API key.
GET /api/v1/typegen/schemanằm sau bức tường Studio access vốn đòi principal là user. API key bị từ chối với403kể cả khi role của nó cóschema:read. Hãy dùng access token của một staff user làm secret lúc build cho typegen, còn API key chỉ-đọc ở Step 4 thì để cho các lượt đọc lúc chạy của trang.
File sinh ra mang tính tất định — header không chứa host, site id hay timestamp — nên cùng một file đã commit verify được trên mọi instance:
- run: npm ci
- run: npx lumibase types --check
env:
LUMIBASE_URL: ${{ secrets.LUMIBASE_URL }}
LUMIBASE_SITE_ID: ${{ secrets.LUMIBASE_SITE_ID }}
LUMIBASE_TOKEN: ${{ secrets.LUMIBASE_TYPEGEN_TOKEN }}
Alternative — the same read with plain fetch
SDK chỉ bọc lại HTTP API; không có gì ngăn bạn gọi thẳng nếu không muốn thêm dependency. Đổi lại bạn mất kết quả có kiểu và mất typegen, đồng thời phải tự dựng URL, header và mã hoá filter:
// app/page.tsx
type Post = { id: string; status: string; data: { title: string; body: string } }
async function getPosts(): Promise<Post[]> {
const url = new URL('/api/v1/items/posts', process.env.LUMIBASE_API_URL)
url.searchParams.set('status', 'published')
// Tham số `filter` nhận hai dạng tương đương — chọn dạng nào cũng được:
// (A) chuỗi JSON: filter={"status":{"_eq":"published"}}
// (B) dạng ngoặc: filter[status][_eq]=published
url.searchParams.set('sort', '-created_at')
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${process.env.LUMIBASE_TOKEN}`,
'X-Lumi-Site': process.env.LUMIBASE_SITE_ID!,
},
// cache kiểu ISR; dùng 'no-store' nếu cần luôn mới
next: { revalidate: 60 },
})
if (!res.ok) throw new Error(`LumiBase responded ${res.status}: ${await res.text()}`)
const json = (await res.json()) as { data: Post[] }
return json.data
}
Troubleshooting
| Triệu chứng | Nguyên nhân khả dĩ | Cách khắc phục |
|---|---|---|
401 Unauthorized | Token thiếu hoặc không hợp lệ | Kiểm tra lại LUMIBASE_TOKEN; tạo lại API key (Bước 4b) |
423 SETUP_REQUIRED | Chưa hoàn tất setup | Hoàn tất http://localhost:1989/setup (Bước 2) |
404 TENANT_NOT_FOUND | Sai X-Lumi-Site — header đúng định dạng nhưng site không tồn tại | Dùng __default__ trừ khi bạn đã tạo site khác |
Mảng trống data: [] | Chưa có bài viết published nào | Đặt trạng thái item thành published trong Studio |
404 trên items | Sai tên collection | Collection phải đặt tên chính xác là posts |
| Lỗi CORS ở trình duyệt | Gọi API từ client code | Gọi từ một Server Component (như hướng dẫn trên) |
429 RATE_LIMITED | Quá nhiều request từ một key/IP | Lùi lại; tuân thủ header Retry-After (xem bên dưới) |
503 RATE_LIMIT_UNAVAILABLE | Cache rate-limit của server gián đoạn (triển khai fail-closed) | Tạm thời — thử lại với lùi thời gian; không phải lỗi app của bạn |
Production & security notes for frontends
Luồng hoạt động ở trên áp dụng tốt cho localhost. Trước khi triển khai thật, hãy đấu nối các hợp đồng mà một client LumiBase được kỳ vọng tuân thủ. Các lưu ý này càng quan trọng hơn khi frontend của bạn là một bản triển khai công khai (Next.js trên Vercel/Cloudflare, v.v.).
1. Giữ API key phía server — luôn luôn
Key lbk_… là một bearer credential. Chỉ đọc nó trong Server Components, Route Handlers, hoặc Server Actions — không bao giờ đọc trong component 'use client' và không bao giờ đặt tiền tố NEXT_PUBLIC_ (vì chúng sẽ bị chèn thẳng vào bundle trình duyệt). Nếu trình duyệt thực sự cần dữ liệu, hãy proxy qua Route Handler của riêng bạn để key luôn nằm ở phía server.
2. Xử lý giới hạn tốc độ (429) và 503 khi fail-closed
LumiBase giới hạn tốc độ theo principal/IP và theo site. Hai phản hồi mà lớp fetch của bạn nên xử lý:
429 RATE_LIMITED— bạn đã vượt quá cửa sổ giới hạn. Response có kèmRetry-After(giây) vàX-RateLimit-Reset. Hãy lùi thời gian; không bắn request liên tục.503 RATE_LIMIT_UNAVAILABLE(LumiBase ≥ 0.24.0) — chỉ trong các bản triển khai bật limiter fail-closed (LUMIBASE_RATE_LIMIT_FAIL_CLOSED=true): cache của limiter tạm thời gián đoạn. Đây là sự cố tạm thời và không phải lỗi app — hãy thử lại với lùi thời gian.
async function lumibaseFetch(url: string | URL, init?: RequestInit, attempt = 0): Promise<Response> {
const res = await fetch(url, init)
if ((res.status === 429 || res.status === 503) && attempt < 3) {
const retryAfter = Number(res.headers.get('Retry-After')) || 2 ** attempt
await new Promise((r) => setTimeout(r, retryAfter * 1000))
return lumibaseFetch(url, init, attempt + 1)
}
return res
}
3. Theo dõi header Deprecation / Sunset (LumiBase ≥ 0.24.0)
Một điểm cuối sắp dừng hoạt động sẽ trả về các header theo chuẩn RFC 8594: Deprecation, Sunset (ngày giờ), và Link rel="deprecation" dẫn tới changelog. Hãy log lại ở phía client để điểm cuối không bất ngờ biến mất:
if (res.headers.get('Deprecation')) {
console.warn('[LumiBase] deprecated endpoint; sunset:', res.headers.get('Sunset'))
}
4. Gọi từ trình duyệt? Hãy cấu hình CORS cẩn thận
Mẫu thiết kế Server-Component ở trên không cần CORS. Nếu bạn buộc phải gọi API từ client code, CMS chỉ cho phép các origin khớp chính xác nằm trong CORS_ALLOWED_ORIGINS — một credentialed response không bao giờ trả về cho wildcard *. Hãy thêm origin frontend của bạn một cách tường minh (ví dụ https://app.example.com), và nhớ rằng các cuộc gọi từ client làm lộ bất kỳ token nào mà nó mang theo, nên hãy dùng token ngắn hạn / phạm vi hẹp chứ không dùng key lbk_….
5. /test-auth là trang thử nghiệm dev-only
Trang xác thực tương tác tại /test-auth là công cụ cho nhà phát triển. Từ LumiBase ≥ 0.24.0 nó sẽ trả về 404 trên production — đừng dựng bất kỳ thứ gì phụ thuộc vào việc nó truy cập được trên host production.
6. Cập nhật bản vá Next.js — cảnh báo SSRF
Vấn đề an toàn môi trường của framework frontend là một phần trong bề mặt tấn công của API. Các bản phát hành Next.js gần đây đã vá các lỗ hổng server-side request forgery (SSRF):
GHSA-89xv-2m56-2m9x— SSRF trong Server Actions trên các custom server.GHSA-p9j2-gv94-2wf4— SSRF trongrewritesqua host đích do kẻ tấn công điều khiển.
Sử dụng next ≥ 16.2.11, và không bao giờ dựng đích cho rewrites/Server-Action từ input người dùng chưa được xác thực (hostname hay URL đầy đủ do người dùng cung cấp). Nếu buộc phải fetch một URL người dùng cung cấp từ phía server, hãy xác minh nó đối với một danh sách cho phép (allowlist) và chặn các dải IP nội bộ/metadata — tương tự như cơ chế phòng thủ SSRF mà LumiBase tự áp dụng.
Compatibility
Tutorial này được ghim vào phiên bản LumiBase tối thiểu và chỉ được xác minh lại khi một hợp đồng API mà nó dựa vào thực sự thay đổi. Chọn dòng khớp với phiên bản LumiBase của bạn (mới nhất ở trên cùng):
| Phiên bản LumiBase | Tutorial này | Ghi chú |
|---|---|---|
| 0.9.0 → mới nhất | ✅ Trang này (đã verify trên 1.0.0-rc.1) | Đăng nhập trả về { data: { token } }; API key qua POST /api/v1/api-keys (tiền tố lbk_); bộ lọc item chấp nhận định dạng JSON và ngoặc vuông; site mặc định __default__. |
| < 0.9.0 | ⚠️ Chưa bao phủ | Các bản cũ hơn trước thời điểm có các hợp đồng trên. Hãy nâng cấp lên ≥ 0.9.0, hoặc điều chỉnh các cuộc gọi auth/filter theo phiên bản của bạn. |
Các hợp đồng mà tutorial này phụ thuộc vào (nếu bất kỳ hợp đồng nào thay đổi trong bản phát hành tương lai, hãy cập nhật bảng ở trên và xác minh lại — xem DoD §5):
POST /api/v1/auth/login→{ data: { token, user } }POST /api/v1/api-keys→{ data: { token: "lbk_…" } }GET /api/v1/items/:collectionbộ lọc chấp nhận cảfilter=<JSON>và dạng ngoặc vuôngfilter[field][_op]=value(JSON ưu tiên hơn nếu gửi cả hai);sort=<csv>GET /api/v1/sitetrả về tenant đang hoạt động; id mặc định__default__lumibase(re-export@lumibase/sdk) —createLumiClient({ url, siteId, token }).with(legacyRest()).items(c).list(...)/.detail(id); mỗi dòng làItemRowvới trường nội dung nằm trong.datalumibase types/types --checkđọcGET /api/v1/typegen/schema, vốn đòi principal là staff user (API key nhận403)- Giới hạn tốc độ trả về
429 RATE_LIMITEDkèmRetry-After; mục "Production & security" bao phủ thêm503 RATE_LIMIT_UNAVAILABLEvà headerDeprecation/Sunset, cả hai được thêm từ0.24.0(luồng cốt lõi ở trên vẫn hoạt động không đổi từ0.9.0)
Next steps
- Tài liệu tham khảo JavaScript SDK — auth, items, files, realtime, Flows.
- Đặc tả API — mọi điểm cuối, bộ lọc, phân trang.
- Tổng quan triển khai — đưa dự án từ localhost lên dev / staging / production (Cloudflare hoặc Docker).