Busabase
Busabase는 AI 에이전트를 위한 오픈소스(MIT) 데이터베이스 및 작업 공간입니다
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Ask your database a question in plain English, get the answer, the chart and the exact SQL.
Live demo: https://ask-data-ai-ten.vercel.app/ — choose Try the demo
([email protected] / askdata-demo), no sign-up needed.
An AI data analyst. Ask a question about a database in plain English and get back a written insight, a chart, the result table, and the exact SQL that produced them.
Built to show how a text-to-SQL agent can be made safe enough to point at a real database: the model's SQL runs through a read-only Postgres role, a parser-based validator, a statement timeout and an enforced row limit.
Stack: Next.js 16 (App Router, Cache Components) · React 19 · TypeScript · Tailwind CSS 4 ·
Vercel AI SDK 7 · Neon Postgres · Prisma 7 · pg · node-sql-parser · Auth.js v5 ·
TanStack Query · Recharts · zod · Vitest
You need Node.js 22 (package.json pins "node": "22.x"), a free
Neon database and a free
Google AI Studio API key.
git clone https://github.com/murtaza008/AskData-AI.git
cd AskData-AI
npm install # also runs `prisma generate`
cp .env.example .env
Fill in .env. Only four values are required:
| Variable | Where it comes from |
|---|---|
DATABASE_URL | Neon dashboard → Connect → pooled connection string |
GOOGLE_GENERATIVE_AI_API_KEY | https://aistudio.google.com/apikey |
AUTH_SECRET | npx auth secret, or any long random string |
ANALYTICS_ROLE_PASSWORD | any long random string (becomes the read-only role's password) |
Important:
.env.exampleships with placeholder values forDIRECT_URLandANALYTICS_DATABASE_URL. Either replace them with real connection strings or set them to"". When they are empty, both are derived automatically fromDATABASE_URL(+ANALYTICS_ROLE_PASSWORD); a leftover placeholder is used as-is and the migration / read-only connection will fail.
Then create everything in the database and start the app:
npm run db:init # migrate app tables → create the read-only role → seed demo data
npm run dev
Open http://localhost:3000 and choose Try the demo. The demo login is
[email protected] / askdata-demo (it is printed on the login page on purpose).
db:init is three steps you can also run one at a time:
| Command | What it does |
|---|---|
npm run db:migrate | Applies the Prisma migration: the app schema (users, conversations, pins, uploads) |
npm run db:setup | Creates the askdata_readonly role and the analytics / uploads schemas, then verifies the role cannot write or read app tables |
npm run db:seed | Builds the demo e-commerce dataset and the demo account. Safe to re-run; it refreshes the data so "last month" is always recent |
Browser Next.js server Postgres (Neon)
─────── ────────────── ───────────────
Chat UI ── POST /api/chat ────▶ auth check ─▶ agent loop
▲ │ getSchema ─────────────────────▶ analytics.* ┐ read-only role
│ NDJSON stream │ runQuery ─▶ validate ─▶ execute ▶ uploads.* ┘ (askdata_readonly)
└── steps + final answer ◀─────┤ submitAnswer
│
Dashboard, history, uploads ────▶ REST routes ─▶ Prisma ────────────▶ app.* owner role
Two database connections, kept apart on purpose:
app schema. The agent never touches this connection.pg pool). Logs in as askdata_readonly, which has
SELECT on the analytics and uploads schemas and nothing else. Every piece of
SQL written by a model, and every stored query a pinned chart re-runs, goes through
this pool.The UI is a static shell (Next.js Cache Components) with client-side data fetching
through TanStack Query, so pages load instantly and the API is the only thing that
reads the session. src/proxy.ts redirects signed-out visitors, and every API
route re-checks the session itself; the proxy is a convenience, not the boundary.
The agent is a multi-step tool-calling loop on the Vercel AI SDK
(src/server/agent/run-agent.ts), capped at 10 model
steps and 50 seconds.
getSchema ──▶ runQuery ──▶ (error? read it, fix the SQL, runQuery again, up to 3 failures) ──▶ submitAnswer
| Tool | What it does |
|---|---|
getSchema | Returns tables, columns, types, primary/foreign keys, column comments (business rules), sample values for text columns, value ranges for numeric/date columns, row counts and today's date. Cached in memory. |
runQuery | Validates and executes one SELECT. Returns the columns, row count and the first 40 rows to the model, or the database's error message so the model can repair the query. |
submitAnswer | Ends the run. Its zod-validated input is the structured answer: answerable, a 2–3 sentence insight, which query result to present, a chart spec (type, x, y, title, yFormat) and follow-up questions. |
Design decisions worth knowing:
toolChoice: "required" on every step, and the only
way to finish is submitAnswer. A structured "answer tool" is more reliable across
providers than asking for JSON in prose.getSchema itself at the start of every run
(it shows as the first step in the trace) and hands the result to the model. That
saves a full model round-trip per question. The tool stays available for re-reading.answerable: false and explains what is missing. The UI shows this as a distinct
"couldn't answer that from this data" state.submitAnswer.src/lib/agent/chart.ts)./api/chat streams to the
browser as newline-delimited JSON, so the trace appears as the agent works.Switching providers is a one-line change per provider in
src/server/ai/models.ts. AI_PROVIDER picks the default;
the model picker in the sidebar lists only providers whose API key is configured.
Each layer works on its own. Breaking one does not expose the database.
Model-written SQL never runs on the app's connection. It runs as askdata_readonly,
created by npm run db:setup. The exact SQL is in
sql/01-readonly-role.sql:
CREATE ROLE askdata_readonly WITH
LOGIN PASSWORD 'CHANGE_ME'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;
ALTER ROLE askdata_readonly SET default_transaction_read_only = on;
ALTER ROLE askdata_readonly SET statement_timeout = '5s';
ALTER ROLE askdata_readonly SET idle_in_transaction_session_timeout = '15s';
ALTER ROLE askdata_readonly SET search_path = analytics;
CREATE SCHEMA IF NOT EXISTS analytics;
CREATE SCHEMA IF NOT EXISTS uploads;
GRANT USAGE ON SCHEMA analytics, uploads TO askdata_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO askdata_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA uploads TO askdata_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA analytics GRANT SELECT ON TABLES TO askdata_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA uploads GRANT SELECT ON TABLES TO askdata_readonly;
REVOKE ALL ON SCHEMA app FROM askdata_readonly;
REVOKE ALL ON ALL TABLES IN SCHEMA app FROM askdata_readonly;
The role has no privileges on the app schema, so the users table and password
hashes are invisible to it. db:setup finishes by logging in as the role and
confirming it cannot create a table or read app."User".
Before execution, src/server/sql/validate.ts parses
the SQL into an AST with node-sql-parser (PostgreSQL grammar) and requires that:
SELECT (CTEs allowed). INSERT, UPDATE, DELETE, DROP,
ALTER, TRUNCATE, GRANT, SET, COPY, SELECT … INTO and FOR UPDATE are rejectedinformation_schema and pg_catalog, including unqualified catalog names and
catalogs "shadowed" by a CTE namepg_*, dblink*, lo_*, set_config,
current_setting, query_to_xml and similar, and any schema-qualified functionQueries run with SET LOCAL statement_timeout (default 5 s, QUERY_TIMEOUT_MS).
Postgres cancels anything slower. The role carries the same limit as a default, so
it applies even if application code forgot.
A validated query is wrapped as SELECT * FROM (<query>) AS askdata_result LIMIT n+1
(default 500, QUERY_ROW_LIMIT). The cap holds whatever LIMIT the model wrote, and
the extra row tells the UI the result was truncated.
Each query runs inside BEGIN TRANSACTION READ ONLY with search_path pinned to
analytics, and is always rolled back.
getSchema describes only the allow-listed tables. The model is never told the app
tables exist.
Database URLs and API keys are read only in modules that import server-only, which
makes importing them from client code a build error. The browser receives model ids
and labels, never keys.
a–z, 0–9, _) and quoted.callbackUrl only accepts same-origin /app paths (no open redirect).DEMO_DAILY_QUESTIONS).uploads
schema. Per-user roles or row-level security would move that into the database.npm run db:seed builds an e-commerce dataset in the analytics schema:
customers, products, orders, order_items, refunds. Roughly 1,400 customers,
46 products, 9,400 orders and 20,000 line items across the last 12 months.
The generator (scripts/lib/dataset.ts) is deterministic
and bakes in patterns so charts have something to show: steady growth across the year, a
November–December peak with a Black Friday week, a few best-sellers and a long tail,
a loyal minority of repeat customers, category seasonality, refund rates that differ
by category, regions of different size, and mobile's share of orders rising.
Column comments carry business rules ("revenue excludes cancelled and refunded
orders"). The agent reads them through getSchema, which is how it learns them.
On Upload data, a CSV is parsed in the browser, column types are inferred, and
you can correct them before anything is sent. The server re-parses the file with the
same inference code, creates a table uploads.u_<user>_<file> and loads the rows.
UPLOAD_MAX_MB), 50,000 rows, 40 columns, 5 tables per userUPLOAD_TTL_DAYS). A daily Vercel Cron job (vercel.json) drops expired tables, and the list endpoint also cleans up on accessSample file. scripts/demo-data/marketing_spend.csv
holds ~200 rows of weekly ad spend by channel and campaign. npm run db:seed loads it
into the demo account as a permanent sample upload (it never expires, can't be
deleted from the UI, doesn't count towards the 5-table limit, and its dates are shifted
so the latest week is last week). That is why "How much did we spend on marketing last
quarter?" works in the demo. On your own account the question is declined until you
upload the file yourself, which shows the agent picking up a new table.
npm run eval
scripts/eval/cases.ts holds 17 questions: 15 answerable
ones (totals, rankings, breakdowns, trends) and 2 that the data cannot answer. Each
answerable case carries a hand-written gold SQL query. The runner sends the
question through the real agent, runs the gold query for the true answer, and checks
the agent's result rows against it. The two unanswerable cases pass only if the agent
declines. Because the truth is computed from the live data, the suite keeps working
after a re-seed.
✔ revenue-last-month 6.1s found 198879.2
✔ top-products-last-month 7.4s matched wireless earbuds pro > merino crew sweater > …
✖ aov-by-channel 5.9s web: expected 229.36, got [231.02]
SQL: SELECT channel, avg(total_amount) …
Accuracy: 16/17 (94%) · avg 6.3s per question
(Illustrative output. Run it against your own key to get real numbers.)
Options: --only <text> filters cases by id, --model <id> picks a model
(for example openai:gpt-5.4-mini), --delay <ms> spaces out calls for free-tier
rate limits, --min <0..1> sets the pass threshold. The process exits non-zero below
the threshold, so it can gate CI.
npm test # unit tests (no database or API key needed)
npm run typecheck
npm run lint
tests/sql-validate.test.ts: about 50 accept/reject cases for the SQL validatortests/agent.test.ts: the agent loop against the AI SDK's mock model: normal flow, self-repair, retry cap, honest refusal, chart repair, provider errorstests/lib.test.ts: chart-spec resolution, CSV type inference, CSV export escaping, callback URL validationDATABASE_URL, DIRECT_URL (the same
Neon string without -pooler in the host), ANALYTICS_ROLE_PASSWORD,
AUTH_SECRET, GOOGLE_GENERATIVE_AI_API_KEY, AI_PROVIDER=google and CRON_SECRET.
Use the same ANALYTICS_ROLE_PASSWORD you ran db:setup with.vercel-build script, which applies migrations before building.npm run db:setup and npm run db:seed once from
your machine with the production DATABASE_URL in .env.https://<your-app>.vercel.app/api/health. It reports ok / failed /
missing for the app database, the read-only role, the demo data, AUTH_SECRET
and the AI provider, without revealing any values, so you can see at a glance
which piece of configuration is wrong. It returns HTTP 503 until every check passes.Notes for the free (Hobby) tier:
/api/chat sets maxDuration = 60, within the Hobby limit.vercel.json schedules the upload clean-up for 04:00 UTC.npm run eval sends 17 questions (roughly 40 to 60 model requests). On a free key,
run it sparingly and raise --delay.Documented in full in .env.example.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL | yes | Owner connection (pooled). App data, migrations, seeding |
DIRECT_URL | no | Non-pooled connection for migrations. Derived from DATABASE_URL if empty |
ANALYTICS_ROLE_PASSWORD | yes | Password of the read-only role |
ANALYTICS_DATABASE_URL | no | Read-only connection string. Derived from the two above if empty |
AUTH_SECRET | yes | Signs session cookies |
AI_PROVIDER | no | Default provider: google (default), openai or anthropic |
GOOGLE_GENERATIVE_AI_API_KEY | one of three | Gemini |
OPENAI_API_KEY | optional | Enables OpenAI models in the picker |
ANTHROPIC_API_KEY | optional | Enables Claude models in the picker |
QUERY_ROW_LIMIT | no | Max rows per query (500) |
QUERY_TIMEOUT_MS | no | Statement timeout (5000) |
UPLOAD_MAX_MB / UPLOAD_TTL_DAYS | no | Upload size limit (4) and lifetime (7) |
DEMO_DAILY_QUESTIONS | no | Daily quota for the demo account (150) |
CRON_SECRET | for cron | Protects the clean-up endpoint |
AUTH_URL | no | Only needed behind a proxy or custom domain |
| Script | What it does |
|---|---|
npm run dev | Start the dev server on http://localhost:3000 |
npm run build / npm start | Production build (prisma generate && next build) and server |
npm run vercel-build | Build used on Vercel: also applies migrations (prisma migrate deploy) |
npm run db:init | db:migrate → db:setup → db:seed in one go |
npm run db:migrate | Apply Prisma migrations (the app schema) |
npm run db:setup | Create and verify the read-only role and the analytics / uploads schemas |
npm run db:seed | (Re)build the demo dataset and the demo account |
npm run eval | Accuracy eval against the live agent (uses your AI quota) |
npm test | Unit tests (Vitest), no database or API key needed |
npm run typecheck | tsc --noEmit |
npm run lint | ESLint |
Every route except /api/health, /api/register and the cron endpoint requires a
signed-in session, and every query is scoped to the caller's user id.
| Method | Route | Purpose |
|---|---|---|
POST | /api/chat | Run the agent for one question; streams NDJSON (start → step… → answer) |
GET | /api/conversations | List the user's conversations |
GET PATCH DELETE | /api/conversations/[id] | Read, rename or delete a conversation |
GET POST | /api/pins | List pinned charts / pin an answer to the dashboard |
DELETE | /api/pins/[id] | Unpin a chart |
GET | /api/pins/[id]/data | Re-run a pinned chart's SQL through the safety pipeline |
GET | /api/overview | Dashboard KPIs (revenue, orders, AOV, repeat rate vs. previous period) |
GET POST | /api/uploads | List uploads / upload a CSV (multipart) |
DELETE | /api/uploads/[id] | Drop an uploaded table |
GET | /api/datasource | Tables the agent can see for this user |
GET | /api/models | Models whose provider key is configured, plus the default |
POST | /api/register | Create an account |
GET | /api/cron/cleanup-uploads | Drop expired uploads (Authorization: Bearer $CRON_SECRET) |
GET | /api/health | Deployment self-check |
prisma/ App schema + migration (schema `app`)
sql/ Read-only role SQL and analytics DDL (readable, runnable by hand)
scripts/
db-setup.ts Create + verify the read-only role
seed.ts Load demo data and the demo account
lib/dataset.ts Deterministic dataset generator
eval/ Eval cases and runner
tests/ Unit tests (validator, agent loop, helpers)
src/
proxy.ts Redirects signed-out visitors (Next.js 16 "proxy")
app/
(marketing)/ Landing page
(auth)/ Login, sign-up
app/ Workspace: chat, conversation, dashboard, upload
api/ Route handlers: chat (streaming), conversations, pins, uploads, models, …
server/ Server-only code
agent/ Agent loop and system prompt
ai/ Provider registry (Gemini / OpenAI / Claude)
sql/ Validator, guarded executor, schema introspection
db/ Prisma client and pg pools
auth/ Auth.js configuration
services/ Conversations, pins, uploads, dashboard overview
features/ UI by feature: chat, dashboard, upload, auth, marketing, workspace
<feature>/components/
<feature>/hooks/
components/ Shared UI: charts, data table, SQL block, buttons, providers
lib/ Isomorphic helpers: types, chart logic, formatting, CSV, validation
The footer's author name and repository link come from src/lib/site.ts;
change them there if you fork the project.
| Symptom | Likely cause and fix |
|---|---|
db:migrate or db:setup cannot connect | DIRECT_URL still holds the .env.example placeholder. Set it to "" or to the non-pooled Neon string |
| Every question fails with a permission or login error | ANALYTICS_ROLE_PASSWORD differs from the one db:setup used, or ANALYTICS_DATABASE_URL is a leftover placeholder. Re-run npm run db:setup (it re-syncs the password) |
| "No AI provider is configured" | No provider key is set. Add GOOGLE_GENERATIVE_AI_API_KEY (or an OpenAI / Anthropic key) |
| "rate limit was reached" / "high demand" | Gemini free-tier limits. Wait a minute, or switch model in the sidebar picker |
| Dashboard shows no data | The demo dataset is missing. Run npm run db:seed |
/api/health returns 503 | Read which check is not ok; the function logs on Vercel show the underlying error |
Built by Murtaza Mazhar — GitHub @murtaza008 · Repository · Live demo