@deployowl/nosql
v2.2.7
Published
Zero-config edge NoSQL SDK for DeployOwl
Readme
@deployowl/nosql
Zero-config Edge NoSQL SDK for DeployOwl.
Use this SDK to store, query, and manage JSON document collections on DeployOwl's global edge infrastructure from Vercel, Render, Cloud Run, Supabase, Railway, Fly.io, or any server environment.
Installation
npm install @deployowl/nosql
# or
pnpm add @deployowl/nosql
# or
yarn add @deployowl/nosqlQuick Start
1. Initialize the client
Construct the client once at module load:
import { createClient } from '@deployowl/nosql';
const db = createClient({
apiKey: process.env.OWL_NOSQL_KEY!, // Your owl_ API key (min 32 chars)
// gatewayUrl: 'https://owlnosql.deployowl.com', // Optional default
// autoHashIds: true, // Auto-hash non-alphanumeric IDs to SHA-256
// cacheQueries: true, // Opt-in: cache identical query() results for 15s (see §4c)
// ulidIds: false, // Set false to require explicit insert() IDs (default: auto-ULID when id omitted)
});
const users = db.collection('users');Security Note:
createClient()must only be run in server environments. Calling it in a browser context will throw an error to prevent API key exposure.
2. Write a document
await users.insert('user_9482', {
email: '[email protected]',
plan: 'pro',
signupAt: Date.now(),
tags: ['early', 'beta']
});
// → { id: 'user_9482', written: true, bytes: 87 }
// Omit the ID to get a time-ordered ULID (sorts chronologically, unique
// without coordination — ideal for feeds, logs, and "latest N" views):
const { id } = await users.insert(null, { body: 'hello', createdAt: Date.now() });
// → { id: '01JD2Q5R8ZKABCDEF012345678', written: true, ... }3. Read a document
const ada = await users.find('user_9482');
// → { email: '[email protected]', plan: 'pro', ... }
// Bypass edge cache for fresh read:
const fresh = await users.find('user_9482', { bypassCache: true });4. Index and Query
query() filters documents server-side by matching field values. It works by scanning index shards — lightweight summaries of each document that store field values for fast lookup.
How indexing works:
- By default (no
index()call), all primitive fields are indexed. Everyinsert()andupdate()writes field values into the index shards automatically.query()can filter by any field without any setup. - If you call
index(['field1', 'field2']), only those fields are indexed. This reduces index size and write overhead for collections with large documents. Fields not in the list are excluded from the index and cannot be used inquery(). index(fields, { covering })inlines listed fields into the index shards. When a query requests only covering fields viafields, results are served straight from the shards — the matching documents are never fetched. This is the fastest way to serve list views: declare the fields your lists display as covering, then query withfields: [...].- Calling
index()automatically reindexes existing documents. You do not need to callreindex()separately after changing the indexed fields — the rebuild happens in the background. reindex()repairs index divergence. It compares the index against stored documents and rebuilds only what diverged (a full rebuild only when divergence is large). Use it if you suspect the index is stale or corrupted, or after manual data imports that bypassed the SDK.
// Option A: Query by any field (no setup needed — all fields indexed by default):
const pros = await users.query({ plan: 'pro' });
const byEmail = await users.query({ email: '[email protected]' });
// Option B: Restrict indexing to specific fields (reduces index size):
await users.index(['email', 'plan']);
// Now only 'email' and 'plan' are in the index. 'name', 'tags', etc. are excluded.
// Existing documents are automatically reindexed in the background — no manual reindex() needed.
// Query by an indexed field:
const pros = await users.query({ plan: 'pro' });
// Guaranteed-fresh query (skips edge cache):
const freshResults = await users.query({ plan: 'pro' }, { bypassCache: true });
// Paginated query — fetch only the first 20 matches (server caps at 500 by default):
const first20 = await users.query({ plan: 'pro' }, { limit: 20 });
const nextPage = await users.query({ plan: 'pro' }, { limit: 20, offset: 20 });
// Sorted query — newest first (sort runs server-side on indexed values):
const newest = await users.query({ plan: 'pro' }, { sort: [{ field: 'created_at', dir: 'desc' }], limit: 10 });
// Field projection — return only the fields your list view displays:
const summaries = await users.query({ plan: 'pro' }, { fields: ['email', 'plan'] });
// Covering query — the fastest list view possible. Declare the fields your
// lists display as covering, then request exactly those fields: results are
// served from the index shards and matching documents are never fetched.
await users.index(['email', 'plan', 'created_at'], { covering: ['email', 'plan', 'created_at'] });
const rows = await users.query({ plan: 'pro' }, { fields: ['email', 'plan', 'created_at'] });Important:
query()only matches fields that are present in the index shards. If you query by a field and get 0 results despite documents existing with that field, either (a) the field value is 64+ chars and was classified as encrypted data, (b)index()was called with a field list that doesn't include the queried field, or (c) the index shards are stale — callreindex()to rebuild.
4b. Count and Batch Fetch
count(filter?) counts matching documents directly from the index — zero document reads. Never export a collection just to call .length:
const total = await users.count();
// → 1240
const proCount = await users.count({ plan: 'pro' });
// → 312findMany(ids) fetches up to 200 documents by ID in one request — one network round trip instead of N parallel find() calls:
const { documents, nextCursor } = await users.findMany(['user_1', 'user_2', 'user_9482']);
// → { documents: [...], nextCursor: null }
// Guaranteed-fresh batch read:
const fresh = await users.findMany(ids, { bypassCache: true });page({ limit, offset }) (experimental) is a client-side pagination bridge — it lists IDs, slices, and batch-fetches the page. Downloads all IDs per call, so prefer query() with limit/offset where possible:
const { documents, total } = await users.page({ limit: 20, offset: 40 });
// → { documents: [...20 docs...], total: 1240 }5. List
list() returns all document IDs in a collection by scanning the index shards.
const ids = await users.list();
// → ['user_1', 'user_2', 'user_9482', ...]
// Guaranteed-fresh (skips edge cache):
const freshIds = await users.list({ bypassCache: true });
list()returns the exact IDs passed toinsert()— no transformation. If you sanitize IDs client-side (e.g. replacing hyphens with underscores), filterlist()results using the same sanitized format.
6. Delete
await users.delete('user_9482');
// → { deleted: true }7. Export (Backups & Migration Only)
Export an entire collection as a JSON array — importable into MongoDB, Postgres, DynamoDB, or any database that accepts JSON.
// Export entire collection:
const result = await users.export();
// → { collection: 'users', count: 1240, total: 1240, offset: 0, limit: null, documents: [...] }Do not use
export()to build list views. It downloads the entire collection on every call. For lists, usequery()withlimit/offset; for counts, usecount().
API Reference
| Method | Signature | Description |
|---|---|---|
| collection(name) | name: string | Get collection handle (alphanumeric, _, -, max 64 chars). |
| find(id, opts?) | id: string, opts.bypassCache?: boolean | Fetch document. Returns T \| null. |
| insert(id, payload, opts?) | id: string \| null, payload: object, opts.ifMatch?: string | Insert document. Omit id (or pass null) for an auto-generated time-ordered ULID. |
| delete(id) | id: string | Delete document. |
| query(filter, opts?) | filter: object, opts.bypassCache?: boolean, opts.limit?: number, opts.offset?: number, opts.sort?: { field: string; dir?: 'asc' \| 'desc' }[], opts.fields?: string[] | Query documents by indexed fields. Returns T[]. limit caps results (server default: 500); offset skips matches for pagination; sort orders results server-side (missing values sort last); fields projects results to the listed fields — when every requested field is a covering field, documents are served from the index without per-document fetches. |
| count(filter?) | filter?: object | Count matching documents from the index — zero document reads. Returns number. |
| findMany(ids, opts?) | ids: string[] (max 200), opts.bypassCache?: boolean | Batch-fetch documents by ID in one request. Returns { documents, nextCursor }. |
| page(opts) (experimental) | opts.limit: number, opts.offset?: number | Client-side pagination bridge. Returns { documents, total }. Prefer query() with limit/offset. |
| list(opts?) | opts.bypassCache?: boolean | List all document IDs in the collection. |
| export(opts?) | opts.limit?: number, opts.offset?: number | Export entire collection as a JSON array. Supports pagination. |
| index(fields, opts?) | fields: string[], opts.covering?: string[] | Create indexes on collection fields. covering inlines the listed fields into the index shards so queries requesting only those fields (via query(..., { fields })) skip per-document fetches entirely. |
| reindex() | — | Repair collection shard indexes: rebuilds only divergent entries (full rebuild only when divergence is large). |
Read consistency
Reads (find, list, query) are served from a per-region edge cache for low latency. Writes (insert, update, delete) invalidate the cached document immediately on the edge that processed the write, so read-your-writes is guaranteed within the same request region.
For guaranteed-fresh reads across all edge locations (e.g. immediately after a write from a different region), pass { bypassCache: true }:
// Guaranteed-fresh — skips the edge cache entirely and reads from storage
const fresh = await users.find('user_9482', { bypassCache: true });
const freshIds = await users.list({ bypassCache: true });
const freshResults = await users.query({ plan: 'pro' }, { bypassCache: true });Opt-in query caching (cacheQueries: true): identical query() calls within 15 seconds are served from the edge cache without recomputation — ideal for dashboards that re-run the same queries on a short poll. A write invalidates the cached results on the write's edge immediately; other edges may serve the cached result for up to 15 seconds. Use { bypassCache: true } on any query that must be guaranteed-fresh. Default is off.
Document & ID Rules
- ID Rules: Alphanumeric,
_,-, max 128 chars. IDs starting with_are reserved. - Auto-Hashing: With
autoHashIds: true, non-conforming IDs are automatically SHA-256 hashed tohash_<base64url>. - Errors: Throws
OwlErrorwith.code,.message,.statusCode.find()swallows 404 and returnsnull.
License
Proprietary © DeployOwl. All rights reserved.
