AI Decision System
Replayable, offline-verifiable records for AI-assisted go/no-go decisions: experiment promotions, typed model answers and human-approved actions.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證AI-powered mock API platform. Describe an endpoint in plain English and get a live, deterministic mock API in seconds. Built with Next.js 14, Prisma & PostgreSQL.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗$ curl -s https://mockbird.app/m/a8f3k2/orders
[
{ "id": "ord_92kd", "customer": "Rohan Mehta", "total": 4820, "status": "shipped" },
{ "id": "ord_71bz", "customer": "Ananya Iyer", "total": 1290, "status": "pending" }
]
A frontend developer needs an endpoint the backend team hasn't written yet, so the screen waits. Mockbird gives them a live URL returning realistic JSON immediately, so the screen gets built today. It also does two things a real backend won't do on request: respond slowly, and fail one call in five. That's how loading and error states actually get tested.
You need Node 18.17+ and Docker (or any Postgres you can point DATABASE_URL at).
docker compose up -d # Postgres on :5432
cp .env.example .env # DATABASE_URL already points at the compose Postgres
npm install # also runs prisma generate
npm run db:push # create the tables
npm run seed # workspace a8f3k2 with four example endpoints
npm run dev # http://localhost:3000
Then, in another terminal:
curl -s http://localhost:3000/m/a8f3k2/orders
curl -s http://localhost:3000/m/a8f3k2/orders/ord_92kd # 1.2s delay, fails 20% of the time
curl -s http://localhost:3000/m/a8f3k2/ordres # 404 that tells you what does exist
GROQ_API_KEY is optional (get a free one at console.groq.com). Without it the Describe it tab falls back to a hand-written skeleton and everything else works exactly the same. The product is fully usable with the AI switched off.
| Variable | Default | What it does |
|---|---|---|
DATABASE_URL | compose Postgres | Postgres connection string. On Neon, use the pooled URL with ?pgbouncer=true |
NEXT_PUBLIC_APP_URL | http://localhost:3000 | Public origin, used to build the mock URLs shown in the UI |
GROQ_API_KEY | unset | Turns on Describe it. Unset means the manual editor only |
AI_MODEL | openai/gpt-oss-20b | Any chat-completion model Groq serves |
UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN | unset | Shared cache and rate limiting across instances. Unset falls back to an in-process map |
MOCK_RATE_LIMIT | 60 | Requests per minute allowed per workspace key on the mock route |
| Command | |
|---|---|
npm run dev | Dev server with hot reload |
npm run build / npm start | Production build and server |
npm test | 39 unit tests: path compiling, matching, substitution, path guessing |
npm run typecheck | tsc --noEmit |
npm run db:push / npm run db:studio | Push the Prisma schema, or browse the data |
npm run seed | The a8f3k2 workspace used in the examples and screenshots |
Every screenshot below is the seeded a8f3k2 workspace, after a handful of the curl calls above.
Pick a method and path, then describe the response in a sentence. The model drafts a JSON Schema and sample records once, at creation, and wires path params like :id to {{id}} so they resolve per request. The preview is editable, and nothing is saved until you press Create endpoint. Prefer to skip the model? Write it takes plain JSON.
Each endpoint has its own status code, delay (up to 8 s) and failure rate, with the status a failed call returns. The strip under the sliders shows the spread you should expect over the next 20 calls, and Send test request fires one from the page. The sidebar lists every route with its specificity, the score that decides which route wins when paths overlap.
The log records what was sent, which route answered and why, and what went back. Injected failures, delayed calls and unmatched paths are labelled inline. The detail pane splits the configured delay from routing time, and any call can be copied as curl or replayed.
This is the part worth reading.
A stored path is never matched as a string. When an endpoint is saved, its path is split into segments and given a numeric specificity, and both are persisted:
"/orders/:id/items" -> [
{ type: "static", value: "orders" },
{ type: "param", name: "id" },
{ type: "static", value: "items" }
]
specificity = (staticSegments * 100) - (paramSegments * 10) - (hasWildcard ? 50 : 0)
At request time the handler asks Postgres for the active endpoints matching (workspaceId, method), ordered by specificity DESC. That query is served by a compound index, so the ordering happens in the database, not in Node, and the matcher itself does no sorting at all. It walks the list and takes the first pattern that fits.
Precedence falls out of the numbers:
| Pattern | Specificity | /orders/new | /orders/ord_92kd | /orders/a/b |
|---|---|---|---|---|
/orders/new | 200 | ✅ wins | — | — |
/orders/:id | 90 | (loses) | ✅ wins | — |
/orders/* | 50 | (loses) | (loses) | ✅ wins |
Matching rules, in src/lib/match.ts:
* matches any depth below it and captures the remainderWhen nothing matches, the 404 body lists every path the workspace actually serves and runs a Damerau-Levenshtein comparison against the requested path, so a typo answers with a suggestion instead of a dead end:
$ curl -s http://localhost:3000/m/a8f3k2/ordres
{
"error": "No endpoint matched",
"requested": "GET /ordres",
"available": ["GET /files/*", "GET /orders", "POST /orders", "GET /orders/:id"],
"didYouMean": "/orders",
"hint": "You called /ordres. Did you mean /orders?"
}
The matcher and the path compiler have 22 unit tests between them, out of 39 in all: npm test.
The stored body may contain tokens that resolve against the request:
| Token | Resolves to |
|---|---|
{{id}} | the matched path param of that name |
{{query.page}} | a query-string value, "" when absent |
{{wildcard}} | everything captured by a trailing * |
{{now}} | ISO timestamp at request time |
{{index}} | position inside the enclosing array, as a number |
{{random.name}}, {{random.int}} | fake data, different every request |
So GET /orders/:id returns a body whose id field matches what was asked for. A string that is nothing but one token takes that token's native type, so "qty": "{{index}}" comes back as 0, not "0".
When the body is an array, ?limit=, ?offset=, ?page=, ?sort=, ?order= and any ?field=value are applied server-side. One mock becomes a usable paginated list endpoint:
curl -s "http://localhost:3000/m/a8f3k2/orders?status=shipped&sort=total&order=desc"
curl -s "http://localhost:3000/m/a8f3k2/orders?limit=2&page=2"
GET /m/a8f3k2/orders/ord_92kd
│
├─ middleware.ts rate limit on the workspace key straight from the URL,
│ before the function does any work
├─ resolve workspace Redis, 60s TTL; falls back to an in-process map
├─ load candidates indexed on (workspaceId, method, specificity DESC)
├─ match route first fit wins; capture params and wildcard
├─ roll failureRate if it fires, return the failure body immediately
├─ sleep(delayMs) capped at 8000ms
├─ substitute tokens then apply collection query params
├─ write RequestLog and increment hitCount
└─ return responseBody with the stored status and headers
No background worker, no websocket, no cron. Every operation completes inside one request. The log trims itself to the most recent 500 rows per workspace inside the write that overflows it, on roughly one call in twenty so the cost is amortised.
Two things, once, at creation time: it turns the English description into a JSON Schema, and it generates sample records matching that schema. The result is shown in an editable field and nothing is written until you press Create endpoint. If the key is missing or the model's reply can't be used, the drawer says so and drops in a hand-written skeleton instead, so creating an endpoint never dead-ends.
At request time there is no model call at all. The response is read from Postgres and returned verbatim, so the same request returns the same bytes every time. That determinism is the whole point. A mock that returns different data on every call is useless to build a UI against, and per-request inference would add seconds of latency and real cost.
| Layer | Choice |
|---|---|
| App | Next.js App Router. One catch-all route handles every mock method and path |
| DB | Postgres via Prisma (Neon's pooled URL with ?pgbouncer=true in production) |
| Cache + rate limits | Upstash Redis over HTTP, so it works in serverless; in-process fallback when unset |
| Identity | A cookie-backed user in src/lib/auth.ts. Everything above it only calls currentUser(), so dropping in Auth.js with a GitHub provider means replacing one function body |
| AI | Any chat completion endpoint, one call at creation |
src/lib/path.ts compile a path into segments + specificity
src/lib/match.ts the matcher and the "did you mean" hint
src/lib/template.ts token substitution
src/lib/collection.ts limit / offset / sort / field filtering
src/lib/endpoints.ts validation and clamping before anything reaches the DB
src/lib/ai.ts the one model call, and the skeleton it falls back to
src/app/m/[key]/[[...path]]/route.ts the one handler that serves every mock
/ creates an actual workspace via POST /api/demo, the same code path as the full editor. It's capped at 10 per hour per visitor and expires in 24 hours, so the curl command a visitor copies keeps working for a day and then quietly 404s.GET returns. Stateful mocks would need per-workspace collection storage and a reset button. That's a deliberate omission, not an oversight.?pgbouncer=true.NEXT_PUBLIC_APP_URL so the UI shows the right mock URLs.MOCK_RATE_LIMIT controls requests per minute per workspace key (default 60).Cannot find module './vendor-chunks/…' in dev. The .next cache is stale, usually after a dev server or build was interrupted. Delete .next and run npm run dev again.docker compose up -d.GROQ_API_KEY. The note under the preview gives the reason, including the free tier's rate limit.