Nuvaka › Developer docs › API reference
API reference
The app injects a global nuvaka object into every UI frame and background script (API v1). Every call returns a Promise; errors are Error objects with a code field.
Contents: basics · storage · shared · net · clipboard · files · notes · cloud · pool · friends · mail · connections · push · ui · events · errors · SDK package
Basics
| Member | Returns | Description |
|---|---|---|
nuvaka.apiVersion | 1 | |
nuvaka.background | boolean | true in the background (QuickJS), false in the UI |
nuvaka.ready | Promise<ctx | null> | in the UI resolves with { page, locale, theme } once the shell connects; in the background resolves immediately with null |
nuvaka.context() | ctx | null | latest context (updated when page, language or theme change) |
nuvaka.info() | { id, version, trust, dev, grants, appVersion } | trust: community, verified, or dev in developer mode (version is dev too); grants: granted scopes, e.g. { "files.read": true } |
nuvaka.me() | { displayName, language, avatarUrl? } | no email or user ID; server call |
nuvaka.settings.get() | { [key]: value } | values of the manifest settings (default when the user has not set one) |
nuvaka.on(name, fn) | unsubscribe function | see events |
nuvaka.off(name, fn) | ||
nuvaka.call(method, arg) | raw call; use the namespaces instead |
nuvaka.storage
The extension's own key-value storage (server, cross-device). No permission needed. Keys are 1–200 characters, / allowed; values are JSON up to 1 MB. Every write carries the version it read (MVCC).
| Call | Returns |
|---|---|
get(key) | { value, version } or null if missing |
set(key, value, { version }) | { version } (the new version). Use version: 0 for a new key. |
delete(key, { version }) | deleting also requires the version read |
update(key, fn) | { value, version }; fn(oldValue | undefined) returns the new value (or a Promise). Returning undefined writes nothing and returns what was read (or null). Up to 5 attempts on conflict. |
list(prefix?, after?) | { items: [{ key, version, size, updatedAt }], next } — no values; pass next as after to continue |
onChanged(fn) | unsubscribe function. fn(key, { value, version, deleted }); key === null means all of the extension's data was deleted. Only newer versions arrive; this context's own writes do not. |
Errors: if_version_required (no version given), version_conflict (e.data = { current, value }), quota_exceeded.
// Paged listing
let after = ''
do {
const page = await nuvaka.storage.list('summary/', after)
for (const it of page.items) console.log(it.key, it.version, it.size)
after = page.next
} while (after)
nuvaka.shared
Named values shared between extensions. Writable names must be in the manifest's shared.exports, readable ones in shared.imports.
| Call | Returns |
|---|---|
set(name, value, { version }) | { version }; version: 0 on first write, value ≤ 1 MB |
getOwn(name) | the value this extension shares: { value, version } or null |
get(from, name) | another extension's value (value only) or null. The user must grant shared and the source extension must be installed. |
nuvaka.net
Needs the net permission. Requests go through the app's proxy: https only, and only to the hosts in the manifest. The browser's fetch is blocked in frames by the CSP.
const r = await nuvaka.net.fetch('https://api.github.com/repos/tauri-apps/tauri', {
method: 'GET', // default GET
headers: { Accept: 'application/vnd.github+json' },
// body: a string or Uint8Array/ArrayBuffer
})
r.status; r.ok; r.url; r.headers['content-type'] // header names are lower case
const data = await r.json() // or r.text(), r.arrayBuffer()
- Request and response bodies up to 10 MB; connect timeout 10 s, total 30 s.
- At most 5 redirects, only to allowed hosts;
AuthorizationandCookieheaders are dropped on redirect. - These headers are not sent:
host,content-length,connection,transfer-encoding,upgrade,proxy-authorization,te,trailer,keep-alive.set-cookieis not passed back. DefaultUser-Agent:NuvakaApps/1. - Names resolving to private network, loopback or link-local addresses are rejected.
- Errors:
permission_required(nonetpermission),net_error(host not allowed, http, timeout, size…). HTTP 4xx/5xx is not an error; checkr.ok.
nuvaka.clipboard
This device's system clipboard. For the cross-device clipboard pool see nuvaka.pool.
| Call | Permission | Returns |
|---|---|---|
readText() | clipboard: read | text or null |
writeText(text) | clipboard: write | null; text ≤ 1 MB |
nuvaka.files
Folders the user opened to the extension on this device. root is the absolute path returned by pick() or folders(); path is relative to it ("notes/a.txt"). .., absolute paths and escaping the root through symbolic links are rejected.
| Call | Permission | Returns |
|---|---|---|
folders() | read | string[] — folders opened on this device |
pick() | read | opens a folder picker; the chosen absolute path, or null if cancelled |
list(root, path?) | read | [{ name, dir, size, mtime }] (mtime in ms or null; symbolic links are skipped; at most 5000 entries) |
readText(root, path) | read | text (UTF-8; invalid bytes become U+FFFD) |
readBytes(root, path) | read | Uint8Array |
writeText(root, path, text) | write | null; missing parent folders are created |
writeBytes(root, path, bytes) | write | null |
mkdir(root, path) | write | null |
remove(root, path) | write | null; a folder is removed only if empty |
The largest readable file is 50 MB. The picked folder itself cannot be modified or removed. Errors: forbidden (folder not opened, or path outside the root), bad_request (invalid path), io (file system error), permission_required.
nuvaka.notes
| Call | Permission | Returns |
|---|---|---|
list({ q?, folder? }) | read | array [{ id, title, content, color, isPinned, createdAt, updatedAt, reminderCount }]; with folder only that folder's notes, and items include folderId. q searches title/content and is filtered on the client (in the SDK). |
get(id) | read | { note: { id, title, content, color, isPinned, folderId, createdAt, updatedAt }, images: [{ id, fileName, fileSize }], reminders: [{ id, reminderType, reminderMessage, remindAt, intervalMinutes, maxReminders, sentCount, isActive }] } |
create({ title, content, color? }) | write | the created note |
update(id, { title?, content?, color?, isPinned? }) | write | the updated note |
delete(id) | write | { success: true } |
Dates are ISO 8601 strings. Note content may be HTML; use textContent when rendering it.
nuvaka.cloud
The user's Nuvaka cloud files; only the folders (or all) the user opened to the extension at install.
| Call | Permission | Returns |
|---|---|---|
roots() | read | { roots: "all" | [{ id, name }] } |
list(folderId?) | read | { items: [{ type: "folder"|"file", id, name, size, contentType, fileId, createdAt, isFrozen, frozenReason, frozenUntil }], breadcrumb: [folders], currentFolderId }. Without folderId, the root (only when roots: "all"). |
read(fileId) | read | Uint8Array (the file item's fileId) |
upload(folderId, name, bytes, contentType?) | write critical | { success, file: { id, fileId, fileName, fileSize, contentType, createdAt } }; up to 100 MB |
delete(fileId) | write critical | { success: true } |
Folders or files outside the scope are rejected with out_of_scope. Writing and deleting are confirmed by the user every time.
nuvaka.pool
The account's cross-device clipboard pool (nuvaka.clipboard permission).
| Call | Permission | Returns |
|---|---|---|
list({ before?, limit? }) | read | { items: [{ id, deviceId, deviceName, kind, mime, fileName, size, hash, createdAt, text, textTruncated, blocks }] } — newest first; limit 1–200 (default 50); before returns items older than this ID |
add(text) | write | the added item (same shape; text up to 4096 characters, longer text is cut and textTruncated: true) |
Adding to the pool is done on behalf of this device; it fails if the device is not registered and active in sync.
nuvaka.friends
list() → { items: [{ username, since }] }. Permission: nuvaka.friends: read.
nuvaka.mail
Account based: call accounts() first. nuvaka.mail.read and .send are critical and confirmed on every use.
| Call | Permission | Returns |
|---|---|---|
accounts() | read or send | { items: [{ id, email, type, isNuvaka }] } (no passwords) |
folders(accountId) | read | array [{ name, fullName, unreadCount, totalCount, isInbox, isSent, isDrafts, isTrash, isSpam, children: [same shape] }] |
messages(accountId, { folder?, page?, pageSize? }) | read | { messages: [{ id, uniqueId, from: { name, email }, to: [{ name, email }], subject, date, isRead, hasAttachments, preview, messageId }], totalCount, page, pageSize, unreadCount }; folder defaults to INBOX, pageSize 1–100 |
get(accountId, folder, uniqueId) | read | { id, uniqueId, from, to, cc, bcc, subject, date, isRead, hasAttachments, textBody, htmlBody, attachments: [{ id, fileName, size, contentType }] } |
send(accountId, { to, cc?, bcc?, subject, body, isHtml?, replyToMessageId? }) | send | { success: true }. to/cc/bcc as a comma-separated string or an array. 30 per hour per extension; every mail sent lands in the user's notifications. |
nuvaka.connections
| Call | Permission | Returns |
|---|---|---|
list() | list or secrets | { items: [{ id, type, name, details: { host, port, username, database … }, createdAt }] } — password/key fields stripped |
secret(id) | secrets critical | { id, type, name, data: { … raw fields including password / privateKey } }; confirmed on every call and added to the user's notification feed |
nuvaka.push
nuvaka.push(title, body?) → { success: true }. A notification to all of the user's devices; nuvaka.push permission. title is required. Subject to the notification limit; notify_limit if the user does not raise it.
nuvaka.ui
| Call | Returns |
|---|---|
notify(title, body?) | null. A local notification on this device; notifications permission; notification limit (notify_limit). Works in the background too. |
theme() | { dark, vars } — in the UI |
page() | the open page's id |
locale() | the app language, e.g. en |
onTheme(fn) | when the theme changes; unsubscribe function |
onPage(fn) | when the user switches to another page from the menu; unsubscribe function |
Events
Listen with nuvaka.on(name, fn). The background receives the events listed in the manifest's triggers; server-originated events are not delivered without the matching read permission.
| Event | Where | Payload |
|---|---|---|
schedule | background | { schedule, at } — at in unix seconds |
app.started, app.focused, network.online | background | null |
notes.changed, cloud.fileAdded, mail.received, clipboard.added | background | a change notice (scope, method, path …); no data, read again through the API |
data.changed | both | { kind, name, version, deleted } — raw event; prefer storage.onChanged |
theme, page, locale | UI | new theme / page ID / language |
// background.js
nuvaka.on('schedule', async ({ schedule, at }) => {
console.log('fired', schedule, new Date(at * 1000).toISOString())
})
nuvaka.on('notes.changed', () => refreshIndex()) // manifest: { "event": "notes.changed" } + nuvaka.notes: read
A Promise returned by a handler is awaited; an error thrown in a handler is written to the app log and does not stop the other handlers.
Errors
Every error is an Error: name: "NuvakaError", code, message (Turkish), and when relevant permission (the missing scope) and data.
| Code | Meaning |
|---|---|
permission_required | no permission, or it was revoked; e.permission is the missing scope |
permission_denied | critical confirmation denied or timed out |
version_conflict | the version read is stale; e.data = { current, value } |
if_version_required | set/delete called without a version |
quota_exceeded | storage quota full |
notify_limit | notification limit reached and the user did not raise it |
rate_limited | rate limit (300 calls per minute, 30 mails per hour) |
confirmation_flood | more than 3 pending confirmations |
out_of_scope | cloud folder not opened to the extension |
not_found | record not found |
forbidden, bad_request, io | local file errors |
net_error, network | net.fetch proxy error / server unreachable |
not_installed, disabled, blocked | extension not installed, stopped or blocked |
session_revoked | the user's session was revoked |
unknown_method | unknown call |
Codes not listed here can also appear; show unknown codes as a generic error.
SDK package (@nuvaka/extension-sdk)
Optional. It contains no transport code: it types the global nuvaka, turns errors into classes and gives you a mock nuvaka for tests.
npm i -D @nuvaka/extension-sdk
import { nuvaka, PermissionError, VersionConflictError, NotifyLimitError } from '@nuvaka/extension-sdk'
import type { Manifest } from '@nuvaka/extension-sdk'
try {
await nuvaka.push('Hello')
} catch (e) {
if (e instanceof NotifyLimitError) { /* try again later */ }
}
| Class | Code |
|---|---|
NuvakaError | base class (any code) |
PermissionError | permission_required |
ConfirmationDeniedError | permission_denied (subclass of PermissionError) |
VersionConflictError | version_conflict (current, value) |
QuotaError | quota_exceeded |
NotifyLimitError | notify_limit |
RateLimitError | rate_limited, confirmation_flood |
The package's nuvaka throws no_runtime when called outside the app (plain browser, Node). Code that uses the global without importing can get types only with import type {} from '@nuvaka/extension-sdk/global'.
Testing: mock nuvaka
import { installMockNuvaka, uninstallMockNuvaka } from '@nuvaka/extension-sdk/testing'
const m = installMockNuvaka({
grants: ['notifications', 'net'], // flat scope names
storage: { count: 1 },
settings: { greeting: 'Hello' },
notifyPerMinute: 3,
fetch: (req) => ({ status: 200, body: { ok: true } }),
})
await nuvaka.storage.update('count', (n) => n + 1)
await m.emit('schedule', { schedule: 'every 15m', at: 1790000000 })
m.advance(60_000) // fake clock (notification window)
uninstallMockNuvaka()
The mock emulates permission checks, critical confirmation (confirm), MVCC, storage.onChanged, the notification limit and the quota. nuvaka-ext test runs vitest in the extension folder.
Nuvaka Apps API v1 · last updated 2026-09-27