Skip to main content

JavaScript SDK reference

This page lists the public API of @taruvi/sdk 1.5.4: each service, its methods, and the HTTP request each method sends. Every service takes the same Client in its constructor. Request paths are relative to your site address, and APP_SLUG is the client's appSlug.

For setup and how requests work, start with JavaScript SDK.

Client#

new Client(options) holds your site address, your app, and how requests authenticate. Create one and pass it to every service.

import {Client} from '@taruvi/sdk';

const taruvi = new Client({
apiUrl: 'https://YOUR_SITE_HOST',
appSlug: 'APP_SLUG',
});

Options

  • apiUrl (required) — Your site address, TARUVI_SITE_URL. A trailing slash is removed.
  • appSlug (required) — The app's slug, TARUVI_APP_SLUG.
  • authMode — 'session', the default, sends the signed-in user's session. 'apiKey' sends apiKey instead; it works only on a server and throws in browsers and React Native.
  • apiKey — An API key, sent as Authorization: Api-Key …. Required with authMode: 'apiKey' and ignored otherwise. Releases before 1.5.4 required a placeholder here.
  • token — A session token, for Node.js and React Native. Ignored in the browser, which reads the token from the sign-in redirect.
  • deskUrl — The host of the hosted sign-in pages. Defaults to apiUrl.
  • detectSessionInUrl — Whether creating the client captures #session_token from the address. Defaults to true. Server-rendered apps set it to false and call handleRedirect().

getConfig() returns a copy of the options. The constructor throws an Error when apiUrl is empty, and logs the SDK version with console.info. Every request sends X-Taruvi-Client: taruvi-js/VERSION (RUNTIME), where RUNTIME is browser, server, or react-native.

Auth#

new Auth(client) sends users to hosted sign-in and manages their session. For the whole flow, see Authenticate with the JavaScript SDK.

import {Auth} from '@taruvi/sdk';

const auth = new Auth(taruvi);

Sign in and sign up#

login(callbackUrl?) and signup(callbackUrl?) send the browser to TaruviBase's hosted sign-in or sign-up page, at /accounts/login/ or /accounts/signup/ on deskUrl. When the user finishes, TaruviBase returns them to callbackUrl, which defaults to the current page without its query string.

auth.login(`${window.location.origin}/dashboard`);

Outside a browser, both log an error and do nothing.

Sign out#

logout(callbackUrl?) forgets the stored session. In a browser, it then sends the user to /accounts/logout/ on deskUrl, which returns them to callbackUrl or, by default, your app's origin. If TaruviBase doesn't accept that address, the user confirms sign-out on TaruviBase's page instead.

await auth.logout();

To forget the session without leaving the page, use clearSession().

Check the session#

GET /_allauth/app/v1/auth/session
if (!(await auth.isUserAuthenticated())) {
auth.login();
}
  • hasToken() — Whether a session token is stored. Sends no request.
  • getSessionToken() — The stored session token, or null.
  • isUserAuthenticated() — Resolves true when TaruviBase accepts the stored session, and false otherwise.
  • validateSession() — The same check, but rejects with AuthError when the session isn't valid.

Handle the sign-in redirect#

handleRedirect(url?) stores the session token that hosted sign-in added to the address, removes the sign-in values from the address bar, and returns the token, or null when there is none. Call it on the page users return to when you create the client with detectSessionInUrl: false.

auth.handleRedirect();

Set or clear the session#

  • setSession(token) — Uses a session token your app got elsewhere, such as a cookie, for later requests.
  • clearSession() — Forgets the stored session without redirecting. To also end it on TaruviBase, use logout().

Get the current user#

GET /api/users/me/
const me = await auth.getCurrentUser();
console.log(me?.data.username);

Returns TaruviResponse<UserData>, or null when no session is stored or the request fails.

Database#

new Database(client).from<T>(table) starts a query on a table, where T types its records. Builder methods return a new builder, and nothing is sent until you call execute(), first(), or count(). Request paths below are relative to /api/apps/APP_SLUG/datatables/TABLE/.

For a PostgreSQL range column in your record type, use the exported PgRangeValue type. It has lower, upper, bounds, and empty fields.

import {Database} from '@taruvi/sdk';

type Task = {id: string; title: string; done: boolean};

const database = new Database(taruvi);

Call an operation such as get(), create(), or delete() last, right before execute(). A builder used wrongly, such as update() without get(id), throws a plain Error before anything is sent.

List records#

Reads the records that match the query. Always set a page size: without pageSize(), a list read returns every matching row.

GET …/data/
await database
.from<Task>('tasks')
.sort('title', 'asc')
.page(1)
.pageSize(20)
.execute();
  • sort(field, order?) — Sorts by field, ascending unless order is 'desc'. Call it again to add a tie-breaker, or pass [{field, order}] to set several at once. Sent as ordering.
  • pageSize(n) — Records per page, up to 1,000 by default; a larger value is rejected with ValidationError. Sent as page_size.
  • page(n) — The page number, starting at 1. Takes effect only with pageSize().

Returns the response envelope: data is the page of records, and total counts every match.

Filter records#

filters(field, operator, value) adds a condition. eq sends the bare field name, and other operators send field__operator. Array values are joined with commas, and repeating a field and operator replaces the earlier value.

GET …/data/?done=false&title__icontains=docs
await database
.from<Task>('tasks')
.filters('done', 'eq', false)
.filters('title', 'icontains', 'docs')
.execute();

For or conditions and nesting, pass a filter tree instead. It's sent as JSON in filters, and TaruviBase combines it with any flat filters using AND:

GET …/data/?filters=…
await database
.from<Task>('tasks')
.filters([
{
operator: 'or',
value: [
{field: 'done', operator: 'eq', value: false},
{field: 'title', operator: 'icontains', value: 'urgent'},
],
},
])
.execute();

operator is a FilterOperator. Choose a filter operator explains each one:

PurposeOperators
Compareeq, ne, gt, gte, lt, lte
Match a setin, nin, ina, nina
Match textcontains, ncontains, icontains, nicontains, containss, ncontainss, startswith, nstartswith, startswiths, nstartswiths, endswith, nendswith, endswiths, nendswiths, like, ilike, search
Match a rangebetween, nbetween
Match nullnull, nnull
Array fieldsacontains, nacontains, acontainedby, nacontainedby, aoverlap, naoverlap, aelement, naelement
Range fieldsrcontains, rcontainedby, roverlaps, radjacent, rstrictleft, rstrictright

Expand related records#

GET …/data/?populate=assignee,project
await database
.from<Task>('tasks')
.populate(['assignee', 'project'])
.execute();
  • populate(fields) — Expands the named relationships.
  • populateAll() — Expands every first-level relationship, with populate=*.

See Relationships.

GET …/data/?search=invoice&fields=id,title
await database
.from<Task>('tasks')
.search('invoice')
.fields('id,title')
.execute();
  • search(query) — Full-text search, on tables that have a search vector.
  • fields(columns) — The columns to return, comma-separated.

Aggregate#

GET …/data/?_aggregate=count(*)&_group_by=done&_having=count__gte=10
await database
.from('tasks')
.aggregate('count(*)')
.groupBy('done')
.having('count__gte=10')
.execute();
  • aggregate(...expressions) — Aggregates such as 'count(*)' or 'sum(total)'. Sent as _aggregate.
  • groupBy(...fields) — Groups the results. Sent as _group_by.
  • having(condition) — Filters the groups by an aggregate alias, such as 'count__gte=10'. Sent as _having.

See Aggregations.

Include allowed actions#

GET …/data/?allowed_actions=update,delete
await database
.from<Task>('tasks')
.allowedActions(['update', 'delete'])
.execute();

Each row gains _allowed_actions: the ones among update and delete that the signed-in user may perform on it. It can't be combined with aggregation.

Get a record#

GET …/data/ID/
await database.from<Task>('tasks').get('TASK_ID').execute();

Returns the envelope, with the record in data. A missing record rejects with NotFoundError.

Get the first record or a count#

await database.from<Task>('tasks').filters('done', 'eq', false).first();
await database.from<Task>('tasks').filters('done', 'eq', false).count();
  • first() — The first matching record, or null. It requests a single row.
  • count() — The number of matching rows. It requests a single row and reads total.

Create records#

POST …/data/
await database
.from<Task>('tasks')
.create([
{title: 'Write release notes', done: false},
{title: 'Ship docs', done: false},
])
.execute();

Pass one record or an array. Returns the envelope; data is an array of the created records, even for one.

Upsert records#

Inserts records, or updates those that already exist.

POST …/data/upsert/?unique_fields=title
await database
.from<Task>('tasks')
.upsert([{title: 'Ship docs', done: true}], ['title'])
.execute();
  • body (required) — One record or an array.
  • uniqueFields — The columns that identify an existing record. Defaults to the primary key.

Returns the envelope; data is {records, count}.

Update records#

get(id) selects a record and update() sends the changes:

PATCH …/data/ID/
await database
.from<Task>('tasks')
.get('TASK_ID')
.update({done: true})
.execute();

Returns the envelope, with the updated record in data.

bulkUpdate(records) updates several records in one request. Each record includes its primary key, and data is {records, count}:

PATCH …/data/
await database
.from<Task>('tasks')
.bulkUpdate([
{id: 'TASK_ID_1', done: true},
{id: 'TASK_ID_2', done: true},
])
.execute();

Delete records#

DELETE …/data/ID/
await database.from('tasks').delete('TASK_ID').execute();
  • delete(id) — Deletes one record. The response body is empty.
  • bulkDelete(ids) — Deletes records by ID, with DELETE …/data/?ids=. Resolves with {deleted_count, message}.
  • deleteFiltered() — Deletes every record that matches the query's filters, with DELETE …/data/?filter=. Throws when no filter is set. When nothing matches, it resolves with the envelope and data: {deleted_count: 0}.
Filtered deletes are permanent

deleteFiltered() removes every matching record in one request and can't be undone. Run the same filters as a read first and check total before you delete.

Graphs and edges#

For tables with parent–child or graph relationships, these methods read a record together with its related records:

GET …/data/ID/?include=descendants&depth=2&format=tree
await database
.from('folders')
.get('ROOT_ID')
.include('descendants')
.depth(2)
.format('tree')
.execute();
  • include(direction) — 'descendants', 'ancestors', or 'both'.
  • depth(n) — The maximum traversal depth.
  • format(format) — 'flat', 'tree', or 'graph'.
  • types(types) — The relationship types to follow. Sent as relationship_type.

edges() targets the table's edge table, TABLE_edges. create(), update(), and delete() then act on edges, and delete(ids) with an array of numbers deletes several:

POST /api/apps/APP_SLUG/datatables/folders_edges/data/
await database
.from('folders')
.edges()
.create([{from_id: 'PARENT_ID', to_id: 'CHILD_ID', type: 'contains'}])
.execute();

See Graphs and hierarchies.

Storage#

new Storage(client).from(bucket) selects a bucket; every method below runs on it. Object paths may contain /, and each segment is URL-encoded. Paths in the request lines are relative to /api/apps/APP_SLUG/storage/buckets/BUCKET/objects/.

import {Storage} from '@taruvi/sdk';
import type {
StorageAccessLinkResponse,
StorageBrowseResponse,
StorageDeleteBatchResponse,
StorageListResponse,
StorageResponse,
StorageUploadBatchResponse,
TaruviResponse,
} from '@taruvi/sdk';

const storage = new Storage(taruvi).from('documents');

execute() resolves with a union of response types. Pass the one you expect, as the examples do, to narrow it; every response type is exported from @taruvi/sdk. For the full workflow, see Storage objects.

List objects#

Lists the objects in the bucket that match filter(). Without filter(), execute() lists every object.

GET …/objects/
await storage
.filter({
prefix: 'reports/',
mimetype: 'application/pdf',
page: 1,
page_size: 50,
})
.execute<StorageListResponse>();

Common filters

  • prefix — Only objects under this path.
  • search — Match file names.
  • mimetype — An exact type, such as application/pdf.
  • mimetype_category — image, video, audio, application, or text.
  • visibility — public or private.
  • page, page_size — Pagination.

StorageFilters also covers size and date ranges and metadata search; see Storage filters.

Returns StorageListResponse: data is a list of StorageObject, and total counts every match.

Browse folders#

Lists the folders and files directly under a prefix, like a file manager.

GET …/objects/browse/
await storage
.browse({prefix: 'reports/', sort: 'name', order: 'asc'})
.execute<StorageBrowseResponse>();

Options

  • prefix — The folder to open. Omit it for the root.
  • sort — name, size, created_at, or updated_at.
  • order — asc or desc.
  • page, page_size — Pagination. page_size defaults to 50, up to 100.

Returns StorageBrowseResponse: data.folders, data.objects, and data.has_next.

Upload files#

Uploads up to 100 files, 100 MB in total, in one request.

POST …/objects/batch-upload/
await storage
.upload({
files: [file],
paths: ['reports/q3.pdf'],
metadatas: [{owner: 'finance'}],
})
.execute<StorageUploadBatchResponse>();

Parameters

  • files (required) — The File objects to upload.
  • paths (required) — The object path for each file, in the same order.
  • metadatas (required) — Metadata for each file, in the same order. Pass {} for none.

Returns StorageUploadBatchResponse. A file that fails doesn't reject the call: data.uploaded_count says how many succeeded, and data.failed lists the rest with the reason.

Download a file#

GET …/objects/PATH/
await storage.download('reports/q3.pdf').execute<Blob>();

Returns a Blob.

Read an object's details#

Reads an object's details without downloading it.

GET …/objects/PATH/?metadata=true
await storage
.metadata('reports/q3.pdf')
.execute<StorageResponse>();

Returns StorageResponse: data is a StorageObject with file_path, size, mimetype, metadata, visibility, and timestamps.

Update metadata#

Replaces an object's metadata.

PATCH …/objects/PATH/
await storage
.update('reports/q3.pdf', {metadata: {owner: 'finance', status: 'final'}})
.execute<StorageResponse>();

Parameters

  • path (required) — The object path.
  • metadata — The new metadata, which replaces the old. Up to 2 KB.

Returns StorageResponse.

An object's visibility always follows its bucket. TaruviBase ignores a visibility sent here; to change it, update the bucket.

Delete objects#

POST …/objects/batch-delete/
await storage
.delete(['reports/q1.pdf', 'reports/q2.pdf'])
.execute<StorageDeleteBatchResponse>();

Returns StorageDeleteBatchResponse. Objects that are missing, or that a policy doesn't let the user delete, don't reject the call: data.deleted_count says how many were deleted, and data.failed lists the rest.

For buckets backed by SharePoint, viewAccess(path) and editAccess(path) return a link that opens an Office file for viewing or editing. editAccess() always needs a signed-in user.

GET …/objects/PATH/edit/
const {data} = await storage
.editAccess('contracts/msa.docx')
.execute<TaruviResponse<StorageAccessLinkResponse>>();
window.open(data.url);

Returns TaruviResponse<StorageAccessLinkResponse>: data.url and data.mode.

Build a download URL#

getUrl(path) returns an object's download URL without sending a request. Private objects still need an authenticated request to that URL.

storage.getUrl('reports/q3.pdf');

Functions#

new Functions(client) runs your app's functions.

Run a function#

POST /api/apps/APP_SLUG/functions/SLUG/execute/
import {Functions} from '@taruvi/sdk';

const functions = new Functions(taruvi);
await functions.execute<{sent: number}>('send-report', {
params: {month: '2026-09'},
});
  • slug (required) — The function's slug.
  • params — The function's input.
  • async — true queues the run and returns right away; false waits for the result. Leave it out to use the function's own mode.

Returns FunctionResponse<T>: data holds the function's return value, and invocation describes the run. A queued run returns an empty list in data; invocation.celery_task_id identifies it. See Execute functions.

Secrets#

new Secrets(client) reads secrets that the signed-in user may see.

import {Secrets} from '@taruvi/sdk';
import type {SecretResponse} from '@taruvi/sdk';

const secrets = new Secrets(taruvi);

Get a secret#

GET /api/secrets/KEY/
await secrets.get('app_config').execute<SecretResponse>();
  • key (required) — The secret's key.
  • app — An app slug, to read that app's secret.
  • tags — Tags the secret must have; if it has none of them, the call rejects with NotFoundError.

Returns SecretResponse once you pass the type to execute(): data holds the key, value, and details.

Get several secrets#

GET /api/secrets/?keys=app_config,feature_flags
await secrets.list(['app_config', 'feature_flags']);
  • keys (required) — Up to 100 keys.
  • app — An app slug, to read that app's secrets.
  • includeMetadata — Return {value, tags, secret_type} for each key instead of the value alone.

Returns the envelope; data maps each key to its value. Keys that are missing, or that the user may not read, are left out.

See Read secrets from code.

Analytics#

new Analytics(client) runs saved queries.

Run a saved query#

POST /api/apps/APP_SLUG/analytics/queries/SLUG/execute/
import {Analytics} from '@taruvi/sdk';

const analytics = new Analytics(taruvi);
await analytics.execute<{month: string; total: number}[]>(
'revenue-by-month',
{params: {year: 2026}},
);
  • querySlug (required) — The saved query's slug.
  • params — Values for the query's parameters.

Returns AnalyticsResponse<T>, with the query's result in data. See Execute saved queries.

A run that fails throws TaruviError with statusCode 400 and code BAD_REQUEST; the cause is in message. See Error categories.

Policy#

new Policy(client) asks TaruviBase what the signed-in user may do. Checks always run as that user.

import {Policy} from '@taruvi/sdk';

const policy = new Policy(taruvi);

Check resources#

Checks several actions on several records in one request.

POST /api/apps/APP_SLUG/check/resources/
const {results} = await policy.checkResource([
{
resource: 'datatable:tasks',
recordId: 'TASK_ID',
attributes: {},
actions: ['update', 'delete'],
},
]);
const canDelete = results[0]?.actions.delete === 'EFFECT_ALLOW';

Each entry takes the policy resource kind, the recordId, the record's attributes (pass {} for none), and the actions to check.

Returns PolicyCheckBatchResult: one result per entry, mapping each action to EFFECT_ALLOW or EFFECT_DENY.

List allowed actions#

POST /api/apps/APP_SLUG/check/resources/
await policy.getAllowedActions(
{kind: 'datatable:tasks', id: 'TASK_ID', attr: {}},
{actions: ['read', 'update', 'delete']},
);
  • resource (required) — {kind, id, attr}.
  • actions — The actions to check. Defaults to read, write, create, update, and delete.
  • auxData — Extra data for the policy.

Returns the names of the allowed actions. The principal option is deprecated: TaruviBase rejects an explicit principal with a 400.

See Runtime permission checks.

User#

new User(client) manages your site's users. Any signed-in user can list and read them. Creating, updating, and deleting users needs organization access (an organization owner, admin, or member, or an API key one of them created) or a Super Admin app role in one of the site's apps. Anyone else gets 403 with code FORBIDDEN, which rejects with ForbiddenError. A Super Admin app role can't change superusers or members of your organization. See Site users.

import {User} from '@taruvi/sdk';

const users = new User(taruvi);

List users#

GET /api/users/
await users.list({search: 'ada', is_active: true, page_size: 20});

Filters: search, is_active, is_superuser, is_deleted, roles (comma-separated role slugs), ordering, page, and page_size. Without filters, the list returns active users that aren't deleted, and leaves out TaruviBase cloud users and superusers. A filter you pass replaces its default: is_active: false lists inactive users.

Returns UserListResponse, with data and total.

Get a user#

GET /api/users/USERNAME/
await users.getUser('ada');

Returns UserResponse.

Create a user#

POST /api/users/
await users.createUser({
username: 'ada',
password: 'CHOOSE_A_PASSWORD',
confirm_password: 'CHOOSE_A_PASSWORD',
first_name: 'Ada',
last_name: 'Lovelace',
role_slugs: ['editor'],
});

username, email, password, confirm_password, first_name, and last_name are required. is_active, attributes, and role_slugs are optional. TaruviBase ignores is_staff and is_cloud_user.

Returns UserResponse.

Update a user#

PUT /api/users/USERNAME/
await users.updateUser('ada', {first_name: 'Augusta'});

Changes only the fields you pass: username, email, first_name, last_name, or is_active. TaruviBase ignores is_staff. Returns UserResponse.

Delete a user#

DELETE /api/users/USERNAME/
await users.deleteUser('ada');

Soft-deletes the user: TaruviBase marks them deleted and inactive, and lists them only when you filter with is_deleted: true. You can't delete yourself.

List a user's apps#

GET /api/users/USERNAME/apps/
await users.getUserApps('ada');

Returns UserAppsResponse: each app's name, slug, display_name, icon, and url.

Assign and revoke roles#

POST /api/assign/roles/
await users.assignRoles({
roles: ['editor'],
usernames: ['ada'],
expires_at: '2026-12-31T23:59:59Z',
});
  • assignRoles({roles, usernames, expires_at?}) — Gives every user in usernames every role in roles, until expires_at when it's set.
  • revokeRoles({roles, usernames}) — Removes them, with DELETE /api/revoke/roles/.

Both need organization access (an organization owner, admin, or member, or an API key one of them created), and take up to 100 roles and 100 usernames. Assignments that already exist, or when revoking don't exist, are skipped and count as success. Returns RolesResponse: a message, plus data.failures when some changes failed, each with username, role, and error.

Read and update preferences#

PUT /api/users/me/preferences/
await users.updatePreferences({theme: 'dark', timezone: 'Europe/London'});

getPreferences() reads the signed-in user's preferences with GET /api/users/me/preferences/. updatePreferences() changes only the fields you pass: date_format, time_format, timezone, theme, or widget_config. Both return UserPreferencesResponse.

App#

new App(client) reads details of the client's app.

import {App} from '@taruvi/sdk';
import type {AppSettingsResponse, RolesListResponse} from '@taruvi/sdk';

const app = new App(taruvi);

List roles#

GET /api/apps/APP_SLUG/roles/
await app.roles().execute<RolesListResponse>();

Returns RolesListResponse once you pass the type to execute().

Read app settings#

GET /api/apps/APP_SLUG/settings/
await app.settings().execute<AppSettingsResponse>();

Returns AppSettingsResponse: the app's display name, colors, icon, banner, and support details.

Settings#

new Settings(client) reads site-wide settings.

import {Settings} from '@taruvi/sdk';

const settings = new Settings(taruvi);

Read public site settings#

GET /api/settings/metadata/
await settings.get<{
domain: string;
settings: Record<string, unknown>;
}>();

Works without a signed-in user. Returns the site's domain and its public settings, such as theme and branding.

Read and update the user-attribute schema#

GET /api/settings/user-attributes/
await settings.getUserAttributes();

updateUserAttributes(schema) replaces the schema, a JSON Schema, with POST /api/settings/user-attributes/. Changing it needs organization access. See User attributes.

Errors#

Every failed request rejects with a TaruviError or one of its subclasses.

ClassstatusCodecode
ValidationError400VALIDATION_ERROR
AuthError401, 410, or 419UNAUTHORIZED
BillingError402, 429, or 503account_suspended, product_suspended, or gate_unavailable
ForbiddenError403FORBIDDEN
NotFoundError404NOT_FOUND
ConflictError409CONFLICT
RateLimitError429RATE_LIMITED
TimeoutError504GATEWAY_TIMEOUT
NetworkError0NETWORK_ERROR
TaruviErrorAny other statusThe platform's code, or INTERNAL_ERROR when it sends none

Every error carries message, and detail when the platform sends it. ValidationError and TaruviError add errors, the problems by field. TaruviError also has data. RateLimitError adds retryAfter, in seconds, from the Retry-After header. ErrorCode exports the code strings.

BillingError means the organization's billing blocked the request: the account isn't active (account_suspended), the plan's usage for module is used up for this period (product_suspended), or billing status couldn't be read (gate_unavailable). Only gate_unavailable is worth retrying; check retryable. A 429 with product_suspended is a BillingError, not a RateLimitError.

Enums#

ExportValues
SortOrderASC 'asc', DESC 'desc'
DataFormatFLAT 'flat', TREE 'tree', GRAPH 'graph'
GraphIncludeDESCENDANTS 'descendants', ANCESTORS 'ancestors', BOTH 'both'
VisibilityPUBLIC 'public', PRIVATE 'private'
MimeTypeCategoryIMAGE, VIDEO, AUDIO, APPLICATION, TEXT

The package also exports the request and response types used on this page, such as TaruviResponse, UserData, FilterOperator, and StorageObject.