@typeb-digital/nucleus-sdk
v0.0.6
Published
Server-side TypeScript SDK for the Nucleus data platform
Downloads
596
Readme
@typeb-digital/nucleus-sdk
Server-side TypeScript SDK for the Nucleus data platform — the company source of truth for people, projects, and clients.
Install this in your app backend (Node.js). It holds the app token and handles all data access.
WARNING — never import this package in browser code. The app token (
NUCLEUS_TOKEN) grants your app's full data access. A browser bundle is public — anyone can extract it. Import@typeb-digital/nucleus-sdkonly in Node.js backend code. For the browser, use@typeb-digital/nucleus-client, which holds no app token and only manages auth sessions.
Installation
npm install @typeb-digital/nucleus-sdk
# or
yarn add @typeb-digital/nucleus-sdkRequires Node.js ≥ 18.
Dual-token architecture
Nucleus apps use two tokens in concert:
| Token | Where it lives | What it does |
| ------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| App token (NUCLEUS_TOKEN) | Your backend only | The ceiling — defines which resources and buckets your app can access at all. Approved when your app registers scopes on the Nucleus dashboard. |
| User access token | Forwarded from the browser via your own API requests | The floor — identifies which user is making the request. Your backend decides what to show that user. |
How it works end to end:
- The frontend uses
@typeb-digital/nucleus-clientto sign the user in and get an access token - Every request from the frontend to your backend includes that token (e.g. in a header like
X-Nucleus-Token) - Your backend receives the user token, validates it, and uses it to enforce user-level authorization in your own logic
- Your backend then calls the Nucleus SDK with the app token to read Nucleus data
What asUser applies to — files only: The server SDK data methods (employees.list(), projects.getById(), etc.) use the app token exclusively and do not accept a user token parameter. The user context floor is enforced by your backend, not by Nucleus SDK data calls. The only place the server SDK accepts a user token is files.getUrl({ asUser }), which checks that the requesting user is in the file's allowedUsers list.
End-to-end example:
// ── Frontend (uses @typeb-digital/nucleus-client) ──────────────────────────
import { nucleus } from './nucleus-client'; // NucleusClient instance
async function loadProjectData(projectId: string) {
const userToken = await nucleus.auth.getAccessToken(); // auto-refreshes if expired
const res = await fetch(`/api/projects/${projectId}`, {
headers: { 'X-Nucleus-Token': userToken },
});
return res.json();
}
// ── Your backend (uses @typeb-digital/nucleus-sdk) ─────────────────────────
import { nucleus } from './nucleus-server'; // NucleusClient instance (app token)
import { isError } from '@typeb-digital/nucleus-sdk';
// Express / Fastify / NestJS handler
async function projectHandler(req, res) {
// 1. Verify the forwarded user token using the server SDK.
// This verifies the RS256 signature against the platform's public key (JWKS),
// cached after the first call — no round-trip per request.
const authResult = await nucleus.auth.verifyToken(req.headers['x-nucleus-token'] ?? '');
if (isError(authResult)) return res.status(401).json({ error: 'Invalid token' });
const { employeeId } = authResult.data;
// 2. Use the server SDK (app token) to read Nucleus data.
// The app token defines what your app can access; your backend decides
// what to return to this specific user.
const result = await nucleus.projects.getById(req.params.projectId, {
expand: ['client', 'projectManager'],
});
if (isError(result)) return res.status(404).json({ error: result.error.message });
// 3. For a user-scoped private file, pass the user token to the server SDK.
// The platform enforces that the user is in the file's allowedUsers list.
const { data: fileData } = await nucleus.files.getUrl('contracts/2026/q1.pdf', {
asUser: req.headers['x-nucleus-token'],
});
return res.json({ project: result.data, contractUrl: fileData?.url });
}Quick start
import { NucleusClient } from '@typeb-digital/nucleus-sdk';
export const nucleus = new NucleusClient({
token: process.env.NUCLEUS_TOKEN!,
scopes: {
employees: ['identity', 'employment'],
projects: ['core'],
clients: ['identity'],
},
});The scopes object is the heart of the type system. Declare it once; every method on the client returns types that reflect exactly those buckets — nothing more, nothing less. TypeScript infers the types automatically; no explicit type parameters needed.
Get your token and init script from the Nucleus dashboard → Apps → your app → Init Script.
Scopes & buckets
Nucleus groups fields into buckets per resource. Your app only receives fields for the buckets you've been approved for.
| Resource | Available buckets |
| -------------- | ------------------------------------------------------------------------------------------------ |
| employees | identity · contact · employment · sensitive¹ · compensation¹ · compensation_history¹ |
| projects | core · team · financials · integrations |
| clients | identity · contact · financials · notes |
| partners | identity · contact · notes |
| departments | core |
| projectTypes | core |
| currencies | core |
| genericRates | core · financials¹ |
¹ Requires platform-team approval.
Data access
Employees
// List — type reflects declared buckets
const result = await nucleus.employees.list({
department: 'Engineering',
page: 1,
pageSize: 50,
});
if (!isError(result)) {
result.data; // Employee[] typed to your scopes
result.meta.total; // number
result.meta.hasMore; // boolean
}
// Single record
const { data: emp } = await nucleus.employees.getById('emp_123');
// With expansion — manager is typed as an Employee object, not just a string ID
const { data: emp } = await nucleus.employees.getById('emp_123', {
expand: ['manager'],
});
if (emp.manager) {
emp.manager.email; // ✓ typed
}Projects
const result = await nucleus.projects.list({ status: 'active' });
// Expand client and team members in one call
const { data: project } = await nucleus.projects.getById('proj_123', {
expand: ['client', 'projectManager', 'memberships.employee'],
});Clients, Partners, Departments, Currencies, Generic Rates
Same pattern — list(params?) and getById(id, options?) on every resource accessor.
Result type
Every method returns a result object — never throws by default:
import { NucleusClient, isError } from '@typeb-digital/nucleus-sdk';
const result = await nucleus.employees.getById('emp_123');
if (isError(result)) {
// result.error.code: 'FORBIDDEN' | 'NOT_FOUND' | 'INVALID_SCOPE' | 'RATE_LIMITED' | 'NETWORK_ERROR'
console.error(result.error.message);
return;
}
// result.data is fully typed here
console.log(result.data.email);File storage
// Upload — app provides a path-style key; upsert semantics
const { data: file } = await nucleus.files.upload({
key: 'users/123/avatar',
file: buffer, // Buffer
mimeType: 'image/png',
visibility: 'private',
allowedUsers: ['emp_123'], // optional — omit for app-wide access
metadata: { type: 'avatar' },
});
// Resolve a URL to pass to your frontend
// Public → CDN URL (instant, no signing)
// Private → fresh short-lived pre-signed URL
const { data } = await nucleus.files.getUrl('users/123/avatar');
// data.url — hand this to the browser
// Private file scoped to a specific user
const { data } = await nucleus.files.getUrl('users/123/avatar', {
asUser: userAccessToken, // from the client SDK's getSession().accessToken
});
// List files
const result = await nucleus.files.list({ prefix: 'users/123/' });
// Soft delete
await nucleus.files.delete('users/123/avatar');User token verification
Verify a Nucleus user access token forwarded from the browser. Uses RS256 local verification — the JWKS is fetched once and cached; no network round-trip per request.
import { NucleusClient, isError } from '@typeb-digital/nucleus-sdk';
const result = await nucleus.auth.verifyToken(userAccessToken);
if (isError(result)) {
// result.error.code: 'FORBIDDEN' (invalid/expired token) | 'NETWORK_ERROR' (JWKS fetch failed)
return res.status(401).json({ error: 'Unauthorized' });
}
const { employeeId, appId, expiresAt } = result.data;
// employeeId — Nucleus employee ID, use as the user identifier in your backend
// appId — app public ID this token was issued for (e.g. 'app_xxx')
// expiresAt — Unix timestamp secondsSelf-service app info
const { data: app } = await nucleus.apps.me();
// app.name, app.scopes (declared), app.status per-bucketPagination
All list() methods return explicit pagination metadata:
const result = await nucleus.employees.list({ page: 2, pageSize: 25 });
if (!isError(result)) {
result.meta.total; // total records
result.meta.page; // current page
result.meta.pageSize; // records per page
result.meta.hasMore; // boolean shortcut
}Expansion
Related records are not included by default. Request them explicitly:
// Depth 1
const { data } = await nucleus.projects.getById('proj_123', {
expand: ['client', 'projectManager'],
});
// Depth 2 (max)
const { data } = await nucleus.projects.getById('proj_123', {
expand: ['projectManager.manager'],
});Valid expand paths per resource are typed — invalid paths or three-segment paths are compile errors.
TypeScript
The SDK is fully typed with no any. Bucket scopes drive the return types at compile time:
// scopes.employees = ['identity'] only
const { data: emp } = await nucleus.employees.getById('emp_123');
emp.email; // ✓ string
emp.phone; // ✗ compile error — contact bucket not declared
emp.managerId; // ✗ compile error — employment bucket not declaredConfiguration
new NucleusClient({
token: process.env.NUCLEUS_TOKEN!, // required
baseUrl: 'https://nucleus.example.com', // optional, defaults to platform URL
scopes: { ... }, // required
})Platform data boundary
Understanding these properties lets you make informed decisions about your app's data model:
Nucleus IDs are opaque string references. emp_123, proj_456 etc. are stable string identifiers. Your app stores them as strings and resolves details via the SDK. There are no foreign keys between your database and Nucleus, and no referential integrity is enforced.
A getById result is a snapshot, not a live reference. The underlying Nucleus record may change after you read it — an employee changes department, a project is archived, a client is renamed. Nothing notifies you.
Nucleus is pull-only today. There is no webhook or change-notification mechanism. Your app must request data to see current state.
The consumption strategy is entirely your choice. Both of these are valid:
- Fetch on demand — call the SDK per-request; data is always fresh; more API calls
- Cache or denormalize into your own database — faster reads; you own keeping it fresh
Nucleus does not prescribe which approach to use. If you cache Nucleus data in your own database, you own the freshness of that cache.
Your app owns its own data entirely. Nucleus stores none of it. The Nucleus SDK only reads Nucleus-managed resources (employees, projects, clients, etc.).
License
MIT — Type B Digital
