hippodb
v0.1.4
Published
TypeScript SDK for HippoDB — manage tables, columns, and rows
Readme
hippodb
TypeScript SDK for HippoDB — a hosted database service with tables, columns, and rows. Designed for server-side use in Node.js 18+. (See Security before using in any web app.)
Installation
npm install hippodbAuthentication
Get a personal access token from HippoDB Settings → Access Tokens. Tokens look like sk-hippo-....
Security — server-side only: Your token grants full read/write access to all your data. Never use it in browser or client-side code — it will be visible in your JavaScript bundle. Always instantiate
HippoDBin a server-side route and expose a narrower API to your frontend.
Security
HippoDB tokens are equivalent to a database password. The SDK must only run on the server (Node.js process, edge function, server action). If you are building a frontend app, create a thin proxy route that holds the token and exposes only what your UI needs.
See Using with Frontend Apps for copy-paste proxy examples for every major platform.
Quick Start
import { HippoDB } from 'hippodb';
const db = new HippoDB({ token: process.env.HIPPODB_TOKEN! });
// Create a table
const table = await db.tables.create({
name: 'Contacts',
columns: [
{ name: 'Name', type: 'text' },
{ name: 'Email', type: 'text' },
{ name: 'Score', type: 'number' },
],
});
const t = db.table(table.id);
// Get column IDs from schema
const schema = await t.schema();
const nameCol = schema.columns.find(c => c.name === 'Name')!.id;
const emailCol = schema.columns.find(c => c.name === 'Email')!.id;
const scoreCol = schema.columns.find(c => c.name === 'Score')!.id;
// Insert rows
await t.rows.upsert([
{ [nameCol]: 'Alice', [emailCol]: '[email protected]', [scoreCol]: 95 },
{ [nameCol]: 'Bob', [emailCol]: '[email protected]', [scoreCol]: 72 },
]);
// Query rows
const rows = await t.rows.query({ limit: 10 });
console.log(rows);Tips for vibe-coding platforms:
- Always use the SDK in a server route (see Security) — never in a component or client file.
- After creating a table, call
await t.schema()to get column IDs (col_xxx) — you need these as keys when inserting/updating row values.
Using with Frontend Apps
Create a thin server-side proxy route that holds your token. Your frontend calls the proxy — never HippoDB directly. Set HIPPODB_TOKEN and HIPPODB_TABLE_ID as environment secrets in your platform's dashboard, never in source code.
Next.js — App Router (v0, Cursor)
// app/api/hippodb/route.ts — runs on the server, never sent to the browser
import { HippoDB } from 'hippodb';
import { NextResponse } from 'next/server';
const db = new HippoDB({ token: process.env.HIPPODB_TOKEN! });
const t = db.table(process.env.HIPPODB_TABLE_ID!);
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const limit = Number(searchParams.get('limit') ?? 20);
const rows = await t.rows.query({ limit });
return NextResponse.json(rows);
}
export async function POST(req: Request) {
const values = await req.json();
const result = await t.rows.upsert([values]);
return NextResponse.json(result);
}// components/MyComponent.tsx — frontend calls your proxy, not HippoDB
const rows = await fetch('/api/hippodb?limit=10').then(r => r.json());Node.js / Express (Replit, Lovable)
// server.ts (or index.js)
import express from 'express';
import { HippoDB } from 'hippodb';
const app = express();
app.use(express.json());
const db = new HippoDB({ token: process.env.HIPPODB_TOKEN! });
const t = db.table(process.env.HIPPODB_TABLE_ID!);
app.get('/api/rows', async (_req, res) => {
const rows = await t.rows.query({ limit: 20 });
res.json(rows);
});
app.post('/api/rows', async (req, res) => {
const result = await t.rows.upsert([req.body]);
res.json(result);
});
app.listen(8080);// Frontend fetch (React, plain JS, etc.)
const rows = await fetch('/api/rows').then(r => r.json());Supabase Edge Functions / Deno (Bolt.new, base44)
// supabase/functions/hippodb-proxy/index.ts ← Bolt.new
// functions/hippodbProxy/entry.ts ← base44
import { HippoDB } from 'npm:hippodb';
const db = new HippoDB({ token: Deno.env.get('HIPPODB_TOKEN')! });
const t = db.table(Deno.env.get('HIPPODB_TABLE_ID')!);
Deno.serve(async (req) => {
if (req.method === 'GET') {
const rows = await t.rows.query({ limit: 20 });
return Response.json(rows);
}
if (req.method === 'POST') {
const values = await req.json();
const result = await t.rows.upsert([values]);
return Response.json(result);
}
return new Response('Method Not Allowed', { status: 405 });
});// Frontend fetch (Bolt.new)
const rows = await fetch('/functions/v1/hippodb-proxy').then(r => r.json());API Reference
new HippoDB(opts)
const db = new HippoDB({
token: string, // required — sk-hippo-... token
baseUrl?: string, // optional — defaults to 'https://api.hippodb.ai'
});Tables
db.tables.list()
Returns all tables for the authenticated user.
const tables = await db.tables.list();
// [{ id: 'tbl_abc123', name: 'Contacts', rowCount: 42, createdAt: '...' }, ...]db.tables.create({ name, columns? })
Creates a new table, optionally with initial columns.
const table = await db.tables.create({
name: 'Products',
columns: [
{ name: 'Title', type: 'text' },
{ name: 'Price', type: 'number' },
{ name: 'In Stock', type: 'boolean' },
],
});
// { id: 'tbl_xxx', name: 'Products', columns: [...], createdAt: '...', updatedAt: '...' }db.tables.delete(tableId)
Permanently deletes a table and all its data.
await db.tables.delete('tbl_abc123');Table Reference
const t = db.table('tbl_abc123');t.schema()
Returns the table's column definitions.
const schema = await t.schema();
// {
// id: 'tbl_abc123',
// name: 'Contacts',
// columns: [
// { id: 'col_xxx', name: 'Name', type: 'text', setting: {}, ... },
// { id: 'col_yyy', name: 'Score', type: 'number', setting: {}, ... },
// ],
// createdAt: '...', updatedAt: '...'
// }Columns
t.columns.add({ name, type, setting? })
Adds a column to the table.
const col = await t.columns.add({ name: 'Status', type: 'select' });
// { id: 'col_zzz', name: 'Status', type: 'select', ... }Column types: 'text' | 'url' | 'number' | 'date' | 'select' | 'boolean' | 'image'
For multi-select: { name: 'Tags', type: 'select', setting: { multiple: true } }
t.columns.update(columnId, { name })
Renames a column.
await t.columns.update('col_xxx', { name: 'Full Name' });t.columns.delete(columnId)
Removes a column and its data from all rows.
await t.columns.delete('col_xxx');Rows
Row values are stored as { [columnId]: value } objects. Get column IDs from t.schema().
t.rows.upsert(rows, opts?)
Inserts or updates rows. Without uniqueBy, all rows are inserted. With uniqueBy, rows matching the specified column(s) are updated; others are inserted.
// Insert
await t.rows.upsert([
{ col_xxx: 'Alice', col_yyy: 95 },
{ col_xxx: 'Bob', col_yyy: 72 },
]);
// → { inserted: 2, updated: 0 }
// Upsert by email column
await t.rows.upsert(
[{ col_email: '[email protected]', col_score: 99 }],
{ uniqueBy: ['col_email'] },
);
// → { inserted: 0, updated: 1 }t.rows.query(opts?)
Queries rows with optional filtering, sorting, and pagination.
const rows = await t.rows.query({
filter: { col_score: { gte: 80 } },
sort: [{ column_id: 'col_score', direction: 'desc' }],
limit: 20,
offset: 0,
});
// [{ id: 'row_xxx', tableId: 'tbl_xxx', values: { col_xxx: 'Alice', col_yyy: 95 }, ... }, ...]t.rows.get(rowId)
Fetches a single row by ID.
const row = await t.rows.get('row_abc123');t.rows.update(rowId, values)
Updates specific fields of a row. Only the provided columns are changed.
await t.rows.update('row_abc123', { col_score: 100 });
// → { id: 'row_abc123', tableId: 'tbl_xxx', values: { col_score: 100, ... }, createdAt: '...', updatedAt: '...' }t.rows.delete(rowId)
Deletes a single row.
await t.rows.delete('row_abc123');
// → { deleted: 1 }t.rows.deleteWhere(filter)
Deletes all rows matching a filter.
await t.rows.deleteWhere({ col_score: { lt: 50 } });
// → { deleted: 3 }t.rows.clear()
Deletes all rows in the table.
await t.rows.clear();
// → { deleted: 42 }Filters
Filters use column IDs as keys and operator objects as values.
type Filter = {
[columnId: string]: {
eq?: unknown; // equal
neq?: unknown; // not equal
gt?: unknown; // greater than
gte?: unknown; // greater than or equal
lt?: unknown; // less than
lte?: unknown; // less than or equal
contains?: string; // substring match (text columns)
in?: unknown[]; // matches any value in array
}
}Examples:
// Exact match
{ col_status: { eq: 'active' } }
// Range
{ col_price: { gte: 10, lte: 100 } }
// Text search
{ col_name: { contains: 'alice' } }
// Multiple values
{ col_country: { in: ['US', 'CA', 'GB'] } }
// Multiple columns (AND logic)
{ col_score: { gte: 80 }, col_status: { eq: 'active' } }Aggregates
Single aggregate
const result = await t.aggregate({
func: 'avg', // 'count' | 'count_distinct' | 'sum' | 'avg' | 'min' | 'max'
columnId: 'col_score',
filter: { col_status: { eq: 'active' } },
});
// → { value: 87.5 }Group by
const result = await t.aggregate({
func: 'count',
groupBy: 'col_status',
});
// → [{ group: 'active', value: 12 }, { group: 'inactive', value: 3 }]Multiple functions
const result = await t.aggregate({
functions: [
{ func: 'count', alias: 'total' },
{ func: 'avg', column_id: 'col_score', alias: 'avg_score' },
{ func: 'max', column_id: 'col_score', alias: 'top_score' },
],
groupBy: 'col_status',
sort: [{ column: 'total', direction: 'desc' }],
limit: 10,
});Error Handling
All methods throw ApiError on failure.
import { HippoDB, ApiError } from 'hippodb';
try {
await t.rows.get('row_doesnotexist');
} catch (err) {
if (err instanceof ApiError) {
console.error(err.status); // HTTP status code (e.g. 404)
console.error(err.message); // Error message
console.error(err.body); // Raw response body
}
}TypeScript Types
All types are exported from hippodb:
import type {
HippoDBOptions,
ColType, ColDef, Column,
TableSummary, TableSchema, CreateTableResult,
Row, UpsertResult, DeleteResult,
Filter, FilterOperator, SortSpec, QueryOptions,
AggregateFunc, AggregateOptions, AggregateFuncSpec, AggregateMultiOptions, AggregateResult,
} from 'hippodb';Complete Example
import { HippoDB } from 'hippodb';
const db = new HippoDB({ token: process.env.HIPPODB_TOKEN! });
async function main() {
// Create table
const { id: tableId } = await db.tables.create({
name: 'Tasks',
columns: [
{ name: 'Title', type: 'text' },
{ name: 'Done', type: 'boolean' },
{ name: 'Priority', type: 'number' },
],
});
const t = db.table(tableId);
// Get column IDs from schema
const schema = await t.schema();
const colId = (name: string) => schema.columns.find(c => c.name === name)!.id;
const titleCol = colId('Title');
const doneCol = colId('Done');
const priorityCol = colId('Priority');
// Insert tasks
await t.rows.upsert([
{ [titleCol]: 'Buy groceries', [doneCol]: false, [priorityCol]: 2 },
{ [titleCol]: 'Write report', [doneCol]: false, [priorityCol]: 1 },
{ [titleCol]: 'Call dentist', [doneCol]: true, [priorityCol]: 3 },
]);
// Query incomplete tasks, sorted by priority
const pending = await t.rows.query({
filter: { [doneCol]: { eq: false } },
sort: [{ column_id: priorityCol, direction: 'asc' }],
});
console.log('Pending tasks:', pending.map(r => r.values[titleCol]));
// Mark first task as done
await t.rows.update(pending[0].id, { [doneCol]: true });
// Count completed tasks
const { value } = await t.aggregate({ func: 'count', filter: { [doneCol]: { eq: true } } }) as { value: number };
console.log('Completed:', value);
}
main();