Database
ZendBX gives every project its own isolated PostgreSQL schema. Full SQL power with a fluent TypeScript query builder on top.
Schemas
Each project gets a dedicated PostgreSQL schema named after the project slug. Tables in zenhire project live under the zenhire schema. This ensures complete data isolation between projects on the same database server.
sql
-- ZendBX creates a schema for each project
-- Your tables live in: <project_slug> schema
-- Example: zenhire project
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'zenhire';CRUD Operations
typescript
import { createClient } from '@zendbx/sdk';
const db = createClient({ apiUrl, anonKey, projectSlug });
// CREATE
const { data: newRow, error } = await db.from('jobs').insert({
title: 'Senior Engineer',
company: 'Acme Corp',
salary: 120000,
});
// READ
const { data: jobs } = await db
.from('jobs')
.select('id, title, company, salary')
.eq('status', 'active')
.order('salary', { ascending: false })
.limit(10);
// UPDATE
const { data: updated } = await db
.from('jobs')
.update({ status: 'closed' })
.eq('id', 'job-uuid');
// DELETE
await db.from('jobs').delete().eq('id', 'job-uuid');Filters
| Method | SQL equivalent | Example |
|---|---|---|
| .eq(col, val) | col = val | .eq('status', 'active') |
| .neq(col, val) | col != val | .neq('role', 'admin') |
| .gt(col, val) | col > val | .gt('salary', 50000) |
| .gte(col, val) | col >= val | .gte('score', 90) |
| .lt(col, val) | col < val | .lt('age', 30) |
| .lte(col, val) | col <= val | .lte('price', 100) |
| .like(col, pat) | col LIKE pat | .like('name', '%john%') |
| .ilike(col, pat) | col ILIKE pat | .ilike('email', '%@gmail%') |
| .is(col, null) | col IS NULL | .is('deleted_at', null) |
| .order(col, opts) | ORDER BY col | .order('created_at', { ascending: false }) |
| .limit(n) | LIMIT n | .limit(20) |
| .range(from, to) | OFFSET / LIMIT | .range(0, 19) |
Pagination
typescript
// Offset pagination
const { data } = await db
.from('jobs')
.select('*')
.range(0, 19); // rows 0-19 (first page of 20)
const { data: page2 } = await db
.from('jobs')
.select('*')
.range(20, 39); // rows 20-39 (second page)
// Cursor pagination (more efficient for large tables)
const { data } = await db
.from('jobs')
.select('*')
.lt('created_at', lastSeenDate)
.order('created_at', { ascending: false })
.limit(20);Schema-Qualified Tables
When calling REST endpoints directly, use dot notation to target tables in a specific schema. The SDK handles this automatically based on your project slug.
typescript
// Access tables in a specific schema
// Use dot notation in the table name
const { data } = await db
.from('zenhire.resumes')
.select('*')
.eq('user_id', userId);💡The REST API also accepts dot notation:
POST /rest/v1/zenhire.resumes. This inserts a row into the resumes table in the zenhire schema.Row Level Security
ZendBX enforces PostgreSQL RLS by default. Every query runs with the user's identity set via SET app.current_user_id. Your policies decide what each user can access.
sql
-- Enable RLS on a table
ALTER TABLE todos ENABLE ROW LEVEL SECURITY;
-- Policy: users can only see their own todos
CREATE POLICY "Users see own todos"
ON todos FOR SELECT
USING (auth.uid() = user_id);
-- Policy: users can insert their own todos
CREATE POLICY "Users insert own todos"
ON todos FOR INSERT
WITH CHECK (auth.uid() = user_id);⚠️The
service_role key bypasses RLS entirely. Never expose it in the browser.REST API Examples
bashINSERT via cURL
curl -X POST https://api.zendbx.in/rest/v1/jobs \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Engineer","company":"Acme","salary":100000}'bashSELECT via cURL
curl "https://api.zendbx.in/rest/v1/jobs?status=eq.active&limit=10&order=salary.desc" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_USER_TOKEN"
