@nuxx-brandly/db-client
v0.5.3
Published
Nuxx BaaS Client SDK
Readme
@nuxx-brandly/db-client
Client SDK for Nuxx BaaS — a lightweight, Supabase-style backend-as-a-service. Every dynamic app gets its own set of tables (declared via a nuxx.json schema file) and talks to them through a single REST endpoint, wrapped by this SDK's fluent query builder.
Install
npm install @nuxx-brandly/db-clientQuick start
import { createClient } from '@nuxx-brandly/db-client';
// url = `${API_URL}/${APP_ID}` — API_URL is your Nuxx DB base (e.g. https://api.nuxx.app/api/nuxx-db),
// APP_ID is the Gitea repo name of your app. apiKey is the per-app secret generated
// from the Nuxx DB admin panel (never the APP_ID itself — that's public, not a secret).
export const db = createClient(`${API_URL}/${APP_ID}`, apiKey);Querying data
Every table is queried the same way, regardless of what columns/relations it has:
// Select all rows
const { data, error } = await db.from('posts').select('*');
// Select specific columns, including a joined relation (declared as a foreignKey in nuxx.json)
const { data, error } = await db.from('posts').select('title, author(name)');
// Filter, sort, paginate
const { data, error } = await db
.from('posts')
.select('*')
.eq('published', true)
.gte('createdAt', '2026-01-01')
.order('createdAt', { ascending: false })
.limit(20)
.offset(0);
// Insert — returns the raw Mongo insert result ({ acknowledged, insertedCount, insertedIds }),
// NOT the created row. Re-select if you need the inserted document back.
const { data, error } = await db.from('posts').insert({ title: 'Hello', authorId: userId });
// Update — always scope it with a filter, or it updates every row in the table
const { data, error } = await db.from('posts').update({ title: 'Edited' }).eq('_id', postId);
// Delete — same rule, always scope it
const { data, error } = await db.from('posts').delete().eq('_id', postId);Every call resolves to { data, error } — data is null and error is populated on failure. The builder is also directly await-able (it implements .then()), so you don't need to call .execute() explicitly — await db.from('x').select('*') works as shown above.
Filter operators: .eq(), .neq(), .gt(), .gte(), .lt(), .lte(), .in(), .contains() (case-insensitive substring match on strings).
Security: tables can require a logged-in end-user
Your api key is not a true secret — it's compiled into your app's public JS bundle, so anyone can extract it from devtools and call this SDK's endpoints directly. Any table declared with "requiresAuth": true in nuxx.json rejects every operation (select/insert/update/delete/bulkUpdate/bulkDelete) unless the caller also has a valid end-user session — you don't need to do anything extra in your own code for this: once db.auth.signIn()/signUp() succeeds, the SDK already attaches that session to every subsequent db.from(...) call automatically. If a query against a requiresAuth table fails with a 401 (error.message mentioning "Token de autorización"), the caller isn't logged in yet — mark any table holding data that shouldn't be world-readable (customers, orders, billing info) this way.
Pagination
.limit()/.offset() work as shown above. There's also an inclusive .range(from, to) convenience (Supabase-style), which is pure sugar over the same two fields — whichever of .range()/.limit()/.offset() you call last wins:
// Rows 0-9 (10 rows), same as .offset(0).limit(10)
const { data, error } = await db.from('posts').select('*').range(0, 9);To get the total row count alongside a page of results, pass { count: 'exact' } to .select():
const { data, error, count } = await db.from('posts').select('*', { count: 'exact' }).range(0, 9);
// count = total rows matching your filters (NOT limited by range/limit) — use it to compute total pages.count reflects rows matching your .eq()/.gt()/etc. filters on the base table — if your select includes a joined relation (author(name)), count is still the number of base rows, not the (potentially different) number of rows after the join. Omitting { count: 'exact' } costs nothing extra — the default select() behavior/performance is unchanged.
Bulk update and bulk delete
.update() always applies the same patch to every row matching your filter (updateMany under the hood) — useful for "set published: true on every row where authorId = X", not for "save 50 rows I edited individually with different values." For that, use .bulkUpdate():
const { data, error } = await db.from('posts').bulkUpdate([
{ id: post1Id, data: { title: 'New title 1' } },
{ id: post2Id, data: { title: 'New title 2', published: true } },
]);
// data: { matchedCount, modifiedCount, results: [{ id, success, error? }, ...] }Partial failure is normal, not an exception: the top-level error stays null even if some records fail (bad id, record not found, invalid data) — always check data.results for per-record outcomes. Records with success: false carry an error string ('not found', 'invalid id format', or a validation/write error message). Valid records are still applied even when others in the same call fail. Max 500 records per call; duplicate ids in one call are not deduplicated (both run, order not guaranteed).
For deleting a list of ids in one call, .bulkDelete() is sugar for .delete().in('_id', ids) — there's no new backend behavior here, since deleting by a filter already deletes every matching row:
const { data, error } = await db.from('posts').bulkDelete([post1Id, post2Id]);All of the above is additive — every existing method, filter, and response shape keeps working exactly as before if you don't use these.
Authenticating end-users
Nuxx DB has its own built-in end-user auth, separate from the app's API key and separate from any table you declare — use db.auth, never hand-roll requests to the REST endpoints:
// Register (roleId is optional — the _id of a row in your own `roles` table, if you declared one)
const { data, error } = await db.auth.signUp({ name: 'Jane', email: '[email protected]', password: 'secret123' });
// Log in
const { data, error } = await db.auth.signIn({ email: '[email protected]', password: 'secret123' });
// Both resolve to { data: { user, token } | null, error }. From this point on, db.from(...) queries
// are automatically authenticated — no need to touch setAuthToken or localStorage yourself, the SDK
// persists the session for you (survives a page reload).
// On app boot, check synchronously (no await) whether someone's already logged in:
const session = db.auth.getSession(); // { user, token } | null
// Re-fetch the current user from the server:
const { data, error } = await db.auth.getUser(); // { data: { user } | null, error }
// Log out:
await db.auth.signOut();The returned user always has the shape { _id, name, email, status, roles, roleId, role } — role is { name, permissions }, resolved automatically when the user has a roleId. Check a permission with user.role?.permissions?.someKey.
users is a reserved table name. Never declare a users table in nuxx.json — end-user accounts already exist via db.auth. Any objectId column that references "the user who owns this row" just declares a normal foreignKey with "foreignTable": "users"; relation population (.select('*, author(name)')) resolves it against the real accounts automatically, excluding password even when selecting *.
Uploading files & sending emails
Use db.files/db.email — never hand-roll requests to the REST endpoints. Under the hood these live on a sibling path (nuxx-api/:appId/... instead of nuxx-db/:appId/...), which the client derives automatically from the URL you passed to createClient.
// Upload a file — store ONLY the returned URL in your table, never the file itself.
const { data, error } = await db.files.upload(fileObject); // data: { url: "https://..." }
// Send an email — recipient/subject/from are bound to the brand server-side, you only send content.
const { data, error } = await db.email.send({ html: '<p>Hello</p>' }); // data: { id: "..." }Uploads are capped at 10MB and restricted to common image/PDF/document mimetypes; email.send's html is sanitized server-side before delivery. Both are rate-limited per app.
Using the AI Assistant
The SDK provides a useAI React hook to interact with an integrated intelligent assistant (powered by Gemini). It handles loading states, error handling, and rate-limiting (20 seconds per request per IP).
import { useAI } from '@nuxx-brandly/db-client';
import { db } from './lib/nuxx-constants'; // Your NuxxDbClient instance
function AIFeature() {
// The second parameter is an optional systemPrompt
const { generate, loading, error, data } = useAI(db);
return (
<div>
<button onClick={() => generate('Write a short poem about Nuxx', 'You are an expert poet.')}>
Generate Poem
</button>
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{data && <p>{data}</p>}
</div>
);
}API reference
createClient(url: string, apiKey: string, options?: { auth?: { token?: string } }) → NuxxDbClient
db.from(table)→{ select, insert, update, delete, bulkUpdate, bulkDelete }db.auth→{ signUp, signIn, signOut, getUser, getSession }— see "Authenticating end-users" above.db.files→{ upload }— see "Uploading files & sending emails" above.db.email→{ send }— see "Uploading files & sending emails" above.db.setAuthToken(token: string | null)— internal;db.auth.signUp/signIn/signOutalready call this for you.
db.from(table).select(columns?) / .insert(data) / .update(data) / .delete() all return a NuxxDbFilterBuilder, chainable with:
.eq(column, value)/.neq(column, value).gt(column, value)/.gte(column, value)/.lt(column, value)/.lte(column, value).in(column, values[]).contains(column, value).order(column, { ascending }).limit(count)/.offset(count)/.range(from, to)
db.from(table).bulkUpdate(updates: { id: string; data: any }[]) → resolves to { data: { matchedCount, modifiedCount, results: { id, success, error? }[] }, error }.
db.from(table).bulkDelete(ids: string[]) → sugar for .delete().in('_id', ids), same NuxxDbFilterBuilder chain.
