@appgaleno/database
v0.3.0
Published
Official GalenoBase database REST client for external webapps.
Downloads
223
Readme
@appgaleno/database
Official GalenoBase database client for external apps.
App user administration (server only)
Use a project-bound gb_live_ Secret API Key with the explicit scope
auth:users:admin. Neither *, db:* nor db:admin grants this permission.
This namespace is available only on the server client; it is separate from the
browser PKCE authentication helper.
const client = createGalenoBaseServerClient({ baseUrl, projectId, apiKey });
await client.authAdmin.suspendUser('app-user-id', { reason: 'Administrative review' });
await client.authAdmin.reactivateUser('app-user-id');These methods call POST /api/v1/auth/users/:userId/suspend and /reactivate.
The suspension reason is trimmed and must contain 1–500 characters; avoid personal
data. Reactivation sends an empty JSON object. Both return
{ data: { id, projectId, status, suspendedAt, suspensionReason } }, with ISO dates
or null. Suspension revokes prior sessions; reactivation requires a fresh login.
Errors retain GalenoBaseApiError status, code and requestId. This documents the
local additive contract and does not establish remote availability.
This package uses the public REST v1 contract. It does not use direct
PostgreSQL access, does not ask for DATABASE_URL, and does not create apps.
Easiest setup: Secure Connect
If you are not technical, start in the GalenoBase Console:
- Open Project Settings > Connect your App.
- Generate the secure command.
- Paste that command in the external app terminal.
The Secure Connect flow writes the server-side environment locally in your app
and creates AI-safe context files. Share only galenobase.integration.json and
galenobase.integration.md with Codex, Cursor, Claude CLI, Antigravity,
Lovable or another AI tool.
Do not paste the Secret API Key in chat, Git, frontend code, NEXT_PUBLIC_*,
VITE_*, PUBLIC_* or REACT_APP_* variables. The SDK helps your backend call
GalenoBase, but the Secret API Key must still stay server-side.
Install
Use this command only from the approved GalenoBase distribution channel:
npm install @appgaleno/databaseServer-side only in this release
Direct browser/public keys are planned for a later release. In this P0, keep all
Secret API Keys server-side only. React, Vue, Flutter Web and no-code frontend
surfaces should call your own backend route, and that backend route should call
GalenoBase with GALENOBASE_API_KEY.
Do not configure VITE_GALENOBASE_READ_KEY, VITE_GALENOBASE_PUBLIC_KEY, or any
live Secret API Key in browser bundles.
Generated schema types
Generate typed helpers in the external app repository:
galenobase pull-schema --collection tasks --out galenobase.schema.json
galenobase gen --schema galenobase.schema.json --out src/lib/galenobase.generated.tsThen use the generated document type with the package:
import { createGalenoBaseServerClientFromEnv } from '@appgaleno/database/server';
import type { TasksDocument } from './lib/galenobase.generated';
const db = createGalenoBaseServerClientFromEnv();
const page = await db.collection<TasksDocument>('tasks').list();Generated files use REST v1 schemas and placeholders only; they never contain
API keys, cookies, DATABASE_URL or direct PostgreSQL credentials.
Server client
Use server-only env vars for reads and writes.
import { createGalenoBaseServerClientFromEnv } from '@appgaleno/database/server';
const db = createGalenoBaseServerClientFromEnv();
await db.collection('tasks').create({ title: 'Ship package', done: false });
await db.collection('tasks').upsert('task-1', { title: 'Ship package', done: false });Document field projection (P2-7)
Browser and server collections accept an optional, readonly fields array on
list, get and getWithVersion:
const tasks = db.collection<{ title: string; done: boolean }>('tasks');
const fields = ['title'] as const;
const page = await tasks.list({ fields, limit: 10 });
const item = await tasks.get('task-1', { fields });
const versioned = await tasks.getWithVersion('task-1', { fields });Projected documents contain only requested, present business fields plus
id, _createdAt and _updatedAt, which are always present. Unknown metadata
timestamps are null. Business properties are typed as partial; calls without
fields retain their existing document type and query serialization. Native
revision still comes from the response header in the same GET request.
Use 1–50 safe top-level names, each up to 128 ASCII letters, digits, underscores or hyphens. Leading hyphens, nested paths, wildcards, exclusions and reserved internal names are rejected. Unknown fields are omitted. The SDK sorts and deduplicates the selection without mutating the supplied array. Pagination must keep the same selection; reordering it is allowed.
Projection does not grant read access or establish field-level permissions.
It runs after existing authorization and sanitization. Responses from create,
update, upsert and _atomic remain complete, even after a projected read.
exportData does not accept fields. This option requires a P2-7-capable server;
older servers reject it. See the full
projection contract.
Optimistic document concurrency (P2-5)
The additive local contract exposes the native database revision through the GalenoBase-Document-Version response header. Business JSON, including _version and ifVersion fields, keeps its meaning. Read the item and its revision together:
const tasks = db.collection('tasks');
const { item, version } = await tasks.getWithVersion('task-1'); // One GET request.
await tasks.update('task-1', { ...item, done: true }, { ifVersion: version });get(id) keeps its existing return shape. update(id, data, options?), upsert(id, data, options?), delete(id, options?) and atomic(id, input, options?) accept { ifVersion: positiveSafeInteger } and send it as a query parameter. Create does not accept a condition. Omit the options to retain last writer wins.
A stale revision raises GalenoBaseApiError with status 409 and code VERSION_CONFLICT without a mutation. Read again and resolve the conflict before retrying. Conditional upsert of a missing item returns 409 without creating it; missing update/delete/atomic targets remain 404 without the P2-6 atomic opt-in below. There is no automatic merge, ETag/If-Match/412 contract or per-row revision in list/read-model results. Call getWithVersion before editing a selected item.
Matching atomic idempotency replays retain the original result and stored version without advancing the counter. Historical replays persisted before migration 0050 may lack responseData.documentVersion: their original result is preserved and GalenoBase-Document-Version is omitted. The service does not substitute the current revision or repeat the mutation to fill that metadata. Changing ifVersion changes the normalized request. Dry-run does not advance revisions. Batch/UOW/import/logical restore writes get native database revisions automatically, without new public per-operation ifVersion conditions. Revisions may have gaps; retained counters prevent reuse after delete/recreate or logical restore. Physical restore/PITR requires clients to resynchronize and must not treat revision as an external clock.
Atomic initialization (P2-6)
The server SDK adds an optional createIfMissing object to the existing
atomic(id, input, options?) call:
await db.collection('daily_counts').atomic('fictional-day', {
createIfMissing: { total: 0, status: 'open' },
increment: { total: 1 },
idempotencyKey: 'fictional-request-001',
});The collection must already exist. Initial values fill only absent top-level
fields; they never reset existing values. A missing document is initialized and
mutated in the same transaction. At least one set or increment is still
required. Initial counters must be safe integers; an existing invalid counter
still fails with 422. Collection rules, schema, project isolation and scopes
remain enforced. A false where condition leaves no initialization behind.
Without the option, missing documents/counters keep their existing 404/422
behavior. With the option and a positive ifVersion, a missing document returns
409 VERSION_CONFLICT, without creation. createIfMissing participates in the
idempotency payload only when provided: use the same initial values on retries.
The SDK does not retry or merge automatically. This additive option requires
a server that supports P2-6; older servers reject the unknown field.
See atomic initialization and document concurrency for migration 0050, rollback compatibility and validation boundaries. This is a local contract, not proof of package publication or remote availability.
Admin SDK (rare and privileged)
@appgaleno/database/admin is a Node-only entrypoint for migrations, user
management, rules, indexes and deliberate privileged automation. It requires the
service-account JSON emitted by the GalenoBase Console and an explicit purpose:
import { readFile } from 'node:fs/promises';
import { createGalenoBaseAdminClient } from '@appgaleno/database/admin';
const credential = JSON.parse(await readFile(process.env.GALENOBASE_ADMIN_CREDENTIAL_FILE!, 'utf8'));
const db = createGalenoBaseAdminClient({
baseUrl: process.env.GALENOBASE_URL!,
credential,
purpose: 'migration',
});Never import this entrypoint in frontend/mobile bundles. Never use an Admin SDK credential as the default credential for Codex, Claude Code, Antigravity or another AI agent; give agents a temporary, minimally scoped Server SDK key instead. Keep the private-key JSON in a server-side secret manager and never in Git, chat or generated integration context.
Required env vars:
GALENOBASE_URL=https://api.example.com
GALENOBASE_PROJECT_ID=project_placeholder
GALENOBASE_API_KEY=<server-side-secret-api-key>Use the minimum API key scope:
db:readfor server-side reads.db:writefor server create/update.db:deletefor server document delete.db:schemafor server schema reads/writes.db:indexesfor server index management.db:viewsfor server saved views.db:adminonly for collection delete.
Hosted Login Auth client
End-user authentication is different from database access. Use the Auth client
in the browser with public project/app identifiers only. Never pass
GALENOBASE_API_KEY, live Secret API Keys, service account secrets or Admin SDK
credentials to this client.
import { createGalenoBaseAuthClient } from '@appgaleno/database/auth';
const auth = createGalenoBaseAuthClient({
projectId: 'project_placeholder',
appId: 'registered_app_id',
redirectUri: 'https://yourapp.com/auth/callback',
authUrl: 'https://galenobase.appgaleno.com',
apiUrl: 'https://api.galenobase.appgaleno.com',
});
document.querySelector('#login')?.addEventListener('click', () => {
void auth.login();
});
const tokens = await auth.handleCallback();
if (tokens) {
// Keep tokens in memory or a secure server-side session strategy.
// Do not log tokens and do not store them in localStorage.
}With no new option, login() keeps the backwards-compatible Hosted form. An
app can also request either experience explicitly:
// The themed Hosted form with every ready method.
await auth.login({ interaction: 'hosted' });
// One app button, then the selected provider (no second provider picker).
await auth.login({ interaction: 'direct', provider: 'google' });
await auth.login({ interaction: 'direct', provider: 'microsoft' });interaction: 'direct' requires an allowlisted provider. The SDK rejects an
unknown or missing provider locally; the server still verifies that direct
login is enabled and that the provider's redirect mode is ready for this
project. A provider passed with interaction: 'hosted' never auto-redirects.
If direct login is unavailable, offer a deliberate Hosted-login action instead
of retrying redirects automatically.
auth.logoutAllSessions(accessToken) encerra a sessão em todos os dispositivos
do usuário do app: o servidor revoga todos os access tokens emitidos e
desconecta todos os sockets ativos desse usuário. O Auth client é stateless;
apague também a cópia local dos tokens depois da chamada.
await auth.logoutAllSessions(tokens.access_token);
tokens = null;Before using Hosted Login, register an app in the GalenoBase console and add the
exact redirect URI to the allowed origins/redirect list. The Auth client uses
PKCE and validates state before token exchange. Transient verifier/state keys
are scoped by project and App ID in sessionStorage; callbacks started by the
previous SDK can still consume its App-ID-only keys once during the compatibility
window.
See the full Hosted/direct login contract and the activation and rollback runbook.
Server-side database parity
The server client mirrors the REST v1 database operations used by external apps:
const tasks = db.collection('tasks');
await tasks.setSchema({
mode: 'strict',
fields: {
title: { type: 'text', required: true },
status: { type: 'select', enum: ['todo', 'done'] },
score: { type: 'number' },
},
});
await tasks.createIndex({ fields: ['status'], mode: 'single' });
await tasks.createView({
name: 'Open tasks',
definition: {
where: [{ field: 'id', op: 'eq', value: 'task-1' }],
visibleFields: ['id', 'title', 'status'],
},
});
await tasks.importData({ items: [{ title: 'A' }, { title: 'B' }] });
await tasks.bulk({
dryRun: false,
idempotencyKey: 'bulk-seed-1',
operations: [{ action: 'create', data: { title: 'C' } }],
});
await db.executeCrossCollectionTransaction({
idempotencyKey: 'confirm-order-123',
dryRun: false,
operations: [
{ collection: 'orders', action: 'update', id: 'order-123', data: { status: 'confirmed' } },
{ collection: 'inventory', action: 'update', id: 'sku-456', data: { available: 9 } },
{ collection: 'reservations', action: 'delete', id: 'reservation-789' },
],
});
await tasks.search({ query: 'Ship', limit: 10, scanLimit: 100 });
await tasks.aggregate({ operations: [{ op: 'count', alias: 'total' }] });
await tasks.join({
include: [{ field: 'ownerId', as: 'owner', limit: 5 }],
limit: 10,
});Use galenobase request for advanced REST v1 calls from a trusted server or CI
environment:
galenobase request --method GET --path /api/v1/db/tasks/_indexes
galenobase request --method POST --path /api/v1/db/tasks/_indexes --body '{"fields":["status"],"mode":"single"}'
galenobase request --method DELETE --path /api/v1/db/tasks/_indexes/index_id --confirm DELETEThe MCP server stays read-only in this release. It can inspect manifest, documents, schemas, indexes, views, search and aggregate results, but it cannot create, update or delete data.
Knowledge retrieval
The server client exposes generic project-scoped Knowledge/RAG retrieval. Use a
server-side API key with db:knowledge:read, db:* or *:
const result = await db.knowledge.retrieve({
namespace: 'product-manuals',
queryText: 'How do I reset a device?',
embeddingModel: 'local-keyword-hash-v1',
topK: 5,
});Production callers can supply a 1536-dimension queryEmbedding instead of
queryText. The SDK never returns the query embedding, and the response marks
that raw questions were not stored. Knowledge ingestion remains a server-side
REST operational surface and is not exposed as a browser SDK operation.
Schema field types
setSchema accepts the public field types:
await db.collection('tasks').setSchema({
mode: 'strict',
fields: {
title: { type: 'text', required: true, minLength: 1 },
dueDate: { type: 'date' },
status: { type: 'select', enum: ['todo', 'done'] },
tags: { type: 'multi_select', enum: ['vip', 'internal'] },
attachment: { type: 'file' },
metadata: { type: 'json' },
owner: { type: 'relation' },
},
});Realtime payload type
The public socket event is db:change. The package exports the TypeScript
payload type without bundling a socket client:
import type { GalenoBaseRealtimeChangePayload } from '@appgaleno/database';
function handleChange(event: GalenoBaseRealtimeChangePayload<{ id: string; title?: string }>) {
if (event.collection !== 'tasks') return;
console.log(event.type, event.id, event.data, event.at);
}