Package Exports
- @batadata/serverless
Readme
@batadata/serverless
SQL-over-HTTP driver for BataDB — a serverless Postgres
platform. Query your database from edge functions, serverless runtimes, or any
environment with fetch, with no TCP connection to manage.
Works in Node.js, Vercel Edge Functions, Cloudflare Workers, Deno, and the browser.
Install
npm install @batadata/serverlessQuick start
import { neon } from '@batadata/serverless';
const sql = neon(
'postgresql://user:pass@ep-xxx.us-east-1.db.batadata.com/mydb',
{
apiKey: process.env.BATA_API_KEY, // bata_... (mint one with `bata api-keys create`)
projectId: 'proj_...',
branchId: 'br_...',
},
);
// Tagged-template queries are automatically parameterized ($1, $2, ...).
const userId = 42;
const rows = await sql`SELECT * FROM users WHERE id = ${userId}`;apiKey and apiUrl also fall back to the BATA_API_KEY and BATA_API_URL
environment variables, so in most deployments you only pass projectId and
branchId:
const sql = neon(connectionString, { projectId, branchId });Explicit queries and full result metadata
The tagged template returns just the rows. For row counts, the executed command,
or column names, use .query():
const result = await sql.query('SELECT id, email FROM users WHERE active = $1', [true]);
result.rows; // Array of row objects
result.rowCount; // number of rows
result.columns; // ['id', 'email']
result.command; // 'SELECT'Pool
A Pool is provided for API compatibility with pg/@neondatabase/serverless.
Over HTTP there is no real connection to pool, so end() is a no-op — it exists
so you can swap drivers without changing call sites.
import { Pool } from '@batadata/serverless';
const pool = new Pool({
connectionString: 'postgresql://user:pass@ep-xxx.db.batadata.com/mydb',
apiKey: process.env.BATA_API_KEY,
projectId: 'proj_...',
branchId: 'br_...',
});
const { rows } = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);
await pool.end();Configuration
neon(connectionString, config) and new Pool(config) accept:
| Option | Type | Default | Description |
|---|---|---|---|
apiKey |
string |
BATA_API_KEY env |
API key, sent as Authorization: Bearer <apiKey>. |
apiUrl |
string |
BATA_API_URL env, else https://api.batadata.com |
Control-plane base URL. The SQL endpoint is ${apiUrl}/v1/sql. |
projectId |
string |
— | Target project (x-project-id header). |
branchId |
string |
— | Target branch (x-branch-id header). |
timeout |
number |
10000 |
Per-request timeout in ms. |
fetchFunction |
typeof fetch |
globalThis.fetch |
Custom fetch (testing / runtimes without a global). |
headers |
Record<string,string> |
— | Extra headers on every request. |
fullResults |
boolean |
true |
Request column/command metadata from the server. |
wakeRetry |
{ enabled?, maxWaitMs? } |
{ enabled: true, maxWaitMs: 30000 } |
Auto-retry a 503 compute-wake response with backoff. See below. |
Pool additionally accepts connectionString, or discrete host / port /
database / user / password / ssl fields.
Compute-wake auto-retry
BataDB branches scale to zero when idle, so the first query after a while can
land on a compute that's still booting. The control plane reports this as a
retryable 503 (code: 'COMPUTE_STARTING') with a Retry-After hint — and
neon() / Pool retry it for you automatically, so "a client that retries
just works" is true out of the box:
const sql = neon(connectionString, {
projectId,
branchId,
wakeRetry: {
enabled: true, // default
maxWaitMs: 30000, // default: total retry budget in ms
},
});
// If the branch is asleep, this call transparently backs off and retries
// instead of throwing — it only surfaces an error if the compute is still
// not ready after the full `maxWaitMs` budget.
const rows = await sql`SELECT * FROM users`;Backoff uses a fast exponential schedule (250ms → 500ms → 1s → …). A small
server Retry-After hint (≤2s — "the compute is just waking") caps each wait
but doesn't slow the schedule down, so a wake that finishes in a few hundred
milliseconds isn't padded to a full second; a larger hint (>2s — deliberate
throttling) is honored verbatim. Retries continue until the total wait budget
is spent. Set wakeRetry: { enabled: false } to disable it and get the old
throw-immediately behavior. Only 503 compute-wake responses are retried —
real SQL errors (syntax errors, constraint violations, timeouts) always throw
immediately, on the first attempt.
Error handling
Failed queries throw a BataError carrying the HTTP status code:
import { BataError } from '@batadata/serverless';
try {
await sql`SELECT 1`;
} catch (err) {
if (err instanceof BataError) {
console.error(err.statusCode, err.message);
// e.g. 401 Query failed with status 401: {"error":"Invalid API key","code":"AUTH_INVALID_KEY"}
}
}TypeScript
Ships with type definitions. Row types are generic:
interface User { id: number; email: string }
const users = await sql<User>`SELECT id, email FROM users`;License
MIT