npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

velocious

v1.0.630

Published

* Concurrent multi threadded web server * Database framework with familiar MVC concepts * Database models with migrations and validations * Database models that work almost the same in frontend and backend * Connection-scoped advisory locks with automatic

Readme

README

  • Concurrent multi threadded web server
  • Database framework with familiar MVC concepts
  • Database models with migrations and validations
  • Database models that work almost the same in frontend and backend
  • Connection-scoped advisory locks with automatic cleanup before pooled connections are reused or closed (see docs/advisory-locks.md)
  • Built-in record auditing for model lifecycle changes (see docs/auditing.md)
  • Declarative state machines for models, with typed event methods generated into the base model (see docs/state-machine.md)
  • Migrations for schema changes and UTC datetime storage, including recorded changeTable batches that combine operations into one ALTER on bulk-capable drivers (see docs/database-migrations.md and docs/change-table.md)
  • Tenant-selected base-model and structure generation with one immutable, fail-closed physical database context; tenant-only model metadata initializes only after that context is active (see docs/tenant-selected-database-generation.md)
  • Read-only tenant migration deploy preflight with stable JSON output and fail-closed ledger reads (see docs/tenant-migration-deploy-preflight.md)
  • External packages (engines) that contribute data models, frontend-model resources and migrations to a consuming app (see docs/packages.md)
  • Optional Rampway-owned durable deployment control plane mounted through the standard routes DSL on Velocious 1.0.577 or newer (see docs/rampway-integration.md)
  • Controllers and views for HTTP endpoints
  • Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses, immutable per-operation remote request context, and one-budget WebSocket startup controls (see docs/frontend-models.md, docs/remote-request-context.md, and docs/websocket-channels.md)
  • Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see the shared-resource sync developer guide and offline sync architecture)
  • Declarative client sync scopes with per-scope cursors, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery, and immutable-handle project clients whose local database state plus remote pull/replay/realtime request context stay tenant-bound through reconnect (see docs/sync-client.md, docs/remote-request-context.md, and docs/offline-sync.md)
  • Reactive useLiveQuery(Model.where(...)) queries for default databases plus immutable-handle tenant live-query sources whose committed events and refreshes stay on the captured physical tenant (see docs/live-queries.md)
  • Server-side sync envelope replay orchestration for app-owned sync receivers, including allowlisted authoritative conflict snapshots that retain submitted aliases in conflict metadata while keying serverModel by canonical model attributes (see docs/sync-envelope-replay-service.md)
  • Self-sustaining sync feeds: upstream imports triggered by the changes pull itself, with framework-owned coalescing and throttling (see docs/sync-upstream-imports.md)
  • AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see the developer guide and proof)
  • SQLite web persistence that automatically prefers OPFS, then IndexedDB, and migrates legacy persisted bytes when possible (see docs/sqlite-web-persistence.md)
  • Bounded frontend tenant SQLite handles with independently deduplicated per-database migrations/model readiness, React lifecycle integration, durable flush/close, backend-complete deletion, clean-only LRU eviction, and scoped pins (see docs/frontend-tenant-sqlite-lifecycle.md)
  • Expo / Metro compatibility guidance and a real Expo export check (see docs/expo-metro-compatibility.md)
  • Gap-less positional lists with automatic reordering via actsAsList, including models with numeric, string, or UUID primary keys (see docs/acts-as-list.md)
  • Rails-style nested-attribute writes on frontend-model save() (see docs/nested-attributes.md)
  • Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, linting, and a process-global reload-retention budget that bounds cache-busted re-import memory (see docs/factories.md)
  • Opt-in Benchmark-style test profiling with privacy-safe rich JSON, directly reusable duration-aware shard manifests, and strict generic shard-profile aggregation (see docs/test-profiling.md)
  • Per-row association counts via .withCount(...), including cohort-safe intersected filters, safe batching of structurally identical aggregates, and automatic IN-list chunking for large parent sets, on frontend and backend queries (see docs/with-count.md)
  • Consumer-defined per-row SQL aggregates/computations via .queryData(...), with compatible projections sharing a roundtrip while preserving declared alias-overwrite order and automatic IN-list chunking for large parent sets, on frontend and backend queries (see docs/query-data.md)
  • Per-record ability checks via .abilities(...) on frontend queries + record.can(action) (see docs/abilities.md)
  • Translated model attributes with current-locale relationship sorting (see docs/translations.md)
  • Cross-process broadcast bus for broadcastToChannel via velocious beacon, including background job runner processes (see docs/beacon.md)
  • Configurable HTTP server worker handlers plus backpressured, descriptor-only file responses with completion callbacks (see docs/http-server.md)
  • Default-on buffered HTTP response compression with Brotli/gzip content negotiation, global and per-response opt-outs, and HEAD-correct representation headers (see docs/http-server.md)
  • Background jobs with Node SQL/TCP workers plus a Browser/Expo local SQLite store and in-process dispatcher, including failure events and authorized database-scoped dashboard count snapshots/deltas. Release-directory integrations have a required release-scoped jobs-main/worker generation and asynchronous retirement compliance target that current startup adoption does not implement end to end (see docs/background-jobs.md, docs/local-background-jobs.md, and docs/background-jobs-dashboard.md)
  • Durable one-off background-job scheduling with exact epoch timestamps (see docs/scheduled-background-job-enqueue.md)
  • Rails-style request and database query logging (see docs/logging.md)
  • EJS-backed mailers with delivery, queueing, and payload rendering support (see docs/mailers.md)
  • Trusted reverse proxy handling for request.remoteAddress() (see docs/trusted-proxies.md)
  • In-process driver schema metadata caching (see docs/schema-metadata-cache.md)
  • Planned local-first shared-resource sync architecture (see docs/offline-sync.md)
  • Selective named database connection checkouts, bounded pool waits, debugging held connections, and checkout-scoped MySQL/MariaDB raw session state (see docs/database-connections.md)
  • Explicit singular-database operation transactions whose model scopes preserve ownership through records, relationships, lifecycle work, nested savepoints, pre-commit guards, and commit callbacks (see docs/operation-scoped-transactions.md)
  • AbortSignal-driven MySQL/MariaDB query cancellation for raw, model, and cross-tenant aggregate queries (see docs/database-query-cancellation.md)
  • Optional built-in debug endpoint for inspecting server and database connection state (see docs/debug-endpoint.md)
  • Optional built-in API manifest endpoint describing every registered frontend-model resource as human- and machine-readable JSON (see docs/api-manifest-endpoint.md)
  • Backend record attachments with filesystem, S3, native callback, and bounded Node path-input persistence (see docs/attachments.md)

Setup

Make a new NPM project.

mkdir project
cd project
npm install velocious
npx velocious init

By default, Velocious looks for your configuration in src/config/configuration.js. If you keep the configuration elsewhere, make sure your app imports it early and calls configuration.setCurrent().

Node SQLite driver

Projects using velocious/build/src/database/drivers/sqlite/index.js must install its optional peer dependencies:

npm install sqlite sqlite3

Projects that do not use the Node SQLite driver do not need these packages. Browser and Expo SQLite drivers use their platform-specific dependencies instead.

Operation-scoped transactions

Use configuration.withTransaction for an atomic unit of model work on one database:

await configuration.withTransaction({databaseIdentifier: "default", name: "accept ticket"}, async (operation) => {
  const ticket = await operation.forModel(Ticket).find(ticketId)

  ticket.setAccepted(true)
  await ticket.save()

  await operation.beforeCommit(async ({operation: guardedOperation}) => {
    const currentTicket = await guardedOperation
      .forModel(Ticket)
      .findByOrFail({id: ticketId})

    if (!currentTicket.acceptanceStillOwnedBy(workerId)) {
      throw new Error("Ticket acceptance ownership changed")
    }
  })

  await operation.afterCommit(async () => {
    await publishAcceptedTicket(ticket.id())
  })
})

Use operation-bound model scopes and their loaded records throughout the callback. operation.beforeCommit runs a final operation-owned guard after callback success but before outer commit or nested savepoint release; a rejection rolls back that frame. operation.transaction adds a nested savepoint, and operation.connection() is the deliberate escape hatch for owned raw SQL. Cross-database models, same-identifier tenant switches to another physical database, and operation handles used after the callback are rejected. On shared SQLite/SQL.js pools, unrelated work waits for the operation lease, while admission during an already-open ordinary transaction is rejected. See operation-scoped transactions for guard, pool, after-commit failure, and migration semantics.

Development

When working on Velocious itself, npm scripts are cross-platform (Windows cmd/PowerShell and POSIX shells):

npm run build
npm run test
npm run test:expo

Maintainers cutting a package release must follow the Velocious release runbook; npm run release:patch commits, pushes, and publishes rather than acting as a local-only version command.

Docker development environment

The checked-in root Dockerfile and compose.yml define one canonical dev service used by humans, CI, and agent systems alike (see docs/docker-development-environment.md). The image is Ubuntu 26.04 LTS (pinned by digest) with Node.js 24.x from signed NodeSource, the universal apt coding/debugging baseline, and the newest published provider CLIs; it is source-independent — no project source is copied and no project dependencies are installed at image build time.

Prerequisites: Docker with the Compose v2 plugin, and this repository checked out at $DEV_HOME_PATH/velocious (default DEV_HOME_PATH: /home/dev).

First-use setup: copy .env.example to the git-ignored .env, set GH_CONFIG_SOURCE_PATH to an existing host GitHub CLI config directory, set AI_PROVIDER_RUNTIME_SOURCE_PATH to the dedicated writable provider runtime, and replace AGENT_CONTEXT_SOURCE_PATH with one exact immutable bundle directory:

cp .env.example .env

$DEV_HOME_PATH must be a dedicated development home that already exists, holds no credentials or secrets, and is owned by (or at least writable by) UID/GID 1000 — the in-container dev user. Do not point it at a general host home directory, and do not recursively chown an existing home; the external environment owns safe initial provisioning.

Normal usage:

docker compose up --build --detach dev
docker compose exec dev bash
scripts/docker-run.sh npm ci   # one-off command in a disposable container

The dev service preserves the complete $DEV_HOME_PATH bind at /home/dev, so dependencies, lane-local caches, settings, and node_modules persist naturally across runs. Codex, Kimi, and OpenCode authentication survives lane recreation through the dedicated provider-runtime bind, while /opt/hermes-agent-context supplies one read-only reviewed guide/skills bundle. Install dependencies with the normal package commands inside the service (for example docker compose exec dev npm ci), never at image build time.

Concurrent isolated instances use the standard Compose project-name contract plus a distinct development home per instance:

COMPOSE_PROJECT_NAME=velocious-review DEV_HOME_PATH=/srv/dev-homes/review \
  docker compose up --build --detach dev

The authorized credential boundaries are the writable provider runtime and the read-only GitHub CLI config. The normal dev service mounts no npm credentials, SSH keys, /opt/data, mutable agent-context discovery links, or broad shared roots. At startup, the provider bootstrap requires real .local and .local/share runtime directories, validates and preserves the provider runtime's canonical relative aliases, serializes shared provider-link migration, then creates exact lane-home provider and /opt/hermes-agent-context discovery links, preserving conflicts only at those managed targets under ~/.provider-runtime-migration-backups/<timestamp>-<unique-suffix>/. The preflight rejects resolved provider-runtime and agent-context sources that contain one another. Threadwire remains parent orchestration, with its provider executable overrides pointed directly at /usr/local/bin/codex, /usr/local/bin/kimi, and /usr/local/bin/opencode. See the setup guide for source-path preflight and the exact lane-local OpenCode state contract.

After changing the Docker artifacts, run the checked-in static contract verifier:

npm run verify:docker-dev-environment

Code quality (fallow)

fallow analyzes the codebase for unused/dead code, duplication, and complexity. CI runs it as a regression gate: it fails only on findings beyond the committed baseline in fallow-baselines/, so existing backlog never blocks a PR but new issues do.

# Regression gate (what CI runs) — fails on new dead code / dupes / complexity beyond the baseline
npm run fallow

# Refresh the baseline after intentionally adding/removing code (commit the updated fallow-baselines/*.json)
npm run fallow:baseline

Baselines are generated against a fresh checkout (no generated dummy configuration.js), so the gate is deterministic in CI and locally. Tuning (entry points, ignores) lives in .fallowrc.json.

Testing

The dummy database configurations require MSSQL_SA_PASSWORD whenever they include the shared MSSQL test database. Set it in the local process environment rather than writing the password into spec/dummy/src/config/configuration*.js. TensorBuzz CI provides one shared test value to both the build and MSSQL service environments.

Tag tests to filter runs.

describe("Tasks", {tags: ["db"]}, () => {
  it("creates a task", {tags: ["fast"]}, async () => {})
})
# Only run tagged tests (focused tests still run)
npx velocious test --tag fast
npx velocious test --include-tag fast,api

# Exclude tagged tests (always wins)
npx velocious test --exclude-tag slow

*.browser-spec.js files remain eligible for both the Node database matrix and the browser runner. Add the reserved browser-only tag only when a suite requires the real browser environment; the Node runner discovers the file but does not execute its tagged tests.

describe("Browser integration", {tags: ["browser-only"]}, () => {
  it("uses browser runtime behavior", async () => {})
})

Target a test by line number or description.

npx velocious test spec/path/to/test-spec.js:34
npx velocious test --example "filters on nested relationship attributes"
npx velocious test --example "/nested.*attributes/i"
npx velocious test --name "filters on nested relationship attributes"

Exclude tags via your testing config file.

// src/config/testing.js
import {configureTests} from "velocious/build/src/testing/test.js"

export default async function configureTesting() {
  configureTests({excludeTags: ["mssql"]})
}

Retry flaky tests by setting a retry count on the test args.

describe("Tasks", () => {
it("retries a flaky check", {retry: 2}, async () => {})
})

Velocious prints the slowest tests after every run so suite hotspots are easy to spot. Each line shows the duration, full description and file:line.

# Default: the 10 slowest tests are printed after the run summary.
npx velocious test

# Report the 25 slowest tests instead.
VELOCIOUS_SLOW_TEST_COUNT=25 npx velocious test

# Disable the report.
VELOCIOUS_SLOW_TEST_COUNT=0 npx velocious test
Slowest 10 tests:
    1145ms  Background jobs - store backfills execution modes for legacy queued jobs (spec/background-jobs/store-spec.js:984)
     913ms  Background jobs - store prunes completed rows past the retention window (spec/background-jobs/store-spec.js:825)
    ...

The report is skipped for single-test runs. See docs/testing-guidelines.md.

Add --profile for a compact Benchmark-style phase and pool summary. Use --profile-json <path> for versioned, privacy-safe detail or --timing-manifest-output <path> to generate a sorted per-file duration map for the existing --timing-manifest shard input; either output flag implies profiling.

npx velocious test --profile-json tmp/test-profile.json \
  --timing-manifest-output tmp/test-timings.json
npx velocious test --groups=4 --group-number=1 \
  --timing-manifest tmp/test-timings.json

# After every shard wrote rich JSON, merge the complete set for the next run
npx velocious test:timing-manifest:merge --output tmp/test-timings.json \
  tmp/profile-1.json tmp/profile-2.json tmp/profile-3.json tmp/profile-4.json

See test profiling for lifecycle accounting, custom activity spans, schema, and privacy guarantees.

Prefer waiting for a real signal or condition over sleeping a fixed duration. waitForEvent(emitter, eventName, {timeoutMs, filter}) resolves the instant a matching event fires (a background job finishing, a model update, a websocket message) and rejects on timeout; for polling an arbitrary condition, use awaitery's waitFor. The stable Velocious import remains velocious/build/src/testing/test.js; its generic waitForEvent primitive comes from @velocious/testing, while the keyed testing DSL and framework runner remain owned by Velocious.

import {waitForEvent} from "velocious/build/src/testing/test.js"
import waitFor from "awaitery/build/wait-for.js"

// Event-driven: resolves as soon as the matching event fires (no polling latency).
const {result} = await waitForEvent(jobRunner, "jobFinished", {filter: (event) => event.jobId === jobId})

// Condition polling: retries the callback until it stops throwing.
await waitFor(() => expect(await Message.count()).toEqual(1))

Velocious captures console output emitted while each test executes, but does not print passing-test output by default. When a test fails, Velocious prints a truncated Console output: block for that failed test and saves the full captured log under tmp/screenshots next to failure screenshots/browser logs/HTML. Each failed test summary prints the saved console log path.

Configure console output behavior in your testing config file.

// src/config/testing.js
import {configureTests} from "velocious/build/src/testing/test.js"

export default async function configureTesting() {
  configureTests({
    consoleOutput: "failure", // default: print captured output only for failed tests
    failedConsoleOutputMaxLines: 200 // default: print the last 200 lines inline
  })
}

Use consoleOutput: "live" to preserve the previous passthrough behavior where test console output is printed while tests run.

Listen for attempt and retry events if you need to reset shared state after a failed attempt or log retry lifecycle details. testAttemptFailed fires after every failed attempt, including the final failed attempt when no retries remain. testRetrying only fires before a retry, and testFailed only fires after retries are exhausted.

import {testEvents} from "velocious/build/src/testing/test.js"

testEvents.on("testAttemptFailed", async ({testDescription, attemptNumber, willRetry}) => {
  console.log(`Failed ${testDescription} attempt ${attemptNumber}`)

  if (willRetry) {
    await resetBrowserOrExternalServices()
  }
})

testEvents.on("testRetrying", ({testDescription, nextAttempt}) => {
  console.log(`Retrying ${testDescription} (attempt ${nextAttempt})`)
})

testEvents.on("testRetried", ({testDescription, attemptNumber}) => {
  console.log(`Retry attempt finished for ${testDescription} (attempt ${attemptNumber})`)
})

Parallel test splitting

Split test files across parallel CI jobs using --groups and --group-number.

# Run group 1 of 4
npx velocious test --groups=4 --group-number=1

# Run group 2 of 4
npx velocious test --groups=4 --group-number=2

# Combine with tags
npx velocious test --groups=3 --group-number=1 --tag fast

# Prefer recorded file durations when balancing groups
npx velocious test --groups=4 --group-number=1 --timing-manifest=tmp/test-timings.json

Files are distributed using a greedy load-balancing algorithm. Each file is primarily weighted by a positive finite duration from the optional timing manifest. Manifest keys are normalized project-relative test paths using / separators:

{
  "spec/system/sign-in-spec.js": 42.7,
  "spec/controller/accounts-spec.js": 8.1
}

Files absent from the manifest and zero-duration entries use the existing deterministic heuristic; stale entries are ignored. When --timing-manifest is explicitly supplied, its file must be readable and contain a plain JSON object with canonical relative paths and finite non-negative durations. Invalid input fails the command even without sharding flags. A compact measured/heuristic/stale summary reports coverage. The heuristic weights files by spec directory (system/ = 20, frontend-models/ = 10, controller/ = 3, default = 1) with a 2x multiplier for .browser-spec.js files. The heaviest files are assigned first to the group with the least accumulated weight, producing balanced wall-clock times across groups.

The algorithm is deterministic: the same file list always produces the same group assignments.

Browser system tests

Run browser compatibility tests via System Testing:

npm run test:browser

Browser system tests must be named *.browser-test.js or *.browser-spec.js (override with VELOCIOUS_BROWSER_TEST_PATTERN). The runner validates and persists the exact Chrome/ChromeDriver pair selected by scripts/prewarm-chromedriver.js, then owns ChromeDriver and Chrome as a managed process group. Startup failures report the runtime paths and versions, service URL, retained logs under tmp/browser-test-chrome/, and Chrome process state before and after cleanup.

Use beforeAll/afterAll for suite-level setup/teardown.

// src/config/testing.js
export default async function configureTesting() {
  beforeAll(async () => {
    // setup shared resources
  })

  afterAll(async () => {
    // teardown shared resources
  })
}

Expectations

Common matchers:

expect(value).toBeTruthy()
expect(value).toMatchObject({status: "success"})
expect({a: 1, b: 2}).toEqual(expect.objectContaining({a: 1}))
expect([1, 2, 3]).toEqual(expect.arrayContaining([2, 3]))

Mailers

Mailers live under src/mailers, with a mailer.js and matching .ejs templates.

import VelociousMailer, {deliveries, setDeliveryHandler} from "velocious/build/src/mailer.js"

class TasksMailer extends VelociousMailer {
  newNotification(task, user) {
    this.task = task
    this.user = user
    this.assignView({task, user})
    return this.mail({to: user.email(), subject: "New task"})
  }
}

Velocious infers the action name from the mailer action method when this.mail(...) is called from that method. Pass actionName explicitly when rendering a different action template or when a shared helper should override the inferred action.

<b>Hello <%= mailer.user.name() %></b>
<p>
  Task <%= task.id() %> has just been created.
</p>

Deliver immediately or enqueue via background jobs:

await new TasksMailer().newNotification(task, user).deliverNow()
await new TasksMailer().newNotification(task, user).deliverLater()

For a provider that advertises duplicate suppression, a producer can require one stable mail operation across outbox replay and native-job retries:

await new TasksMailer().newNotification(task, user).deliverLater({
  deliveryOperation: {
    id: `project-command:${command.id()}`,
    idempotency: "required"
  }
})

Required delivery fails before enqueue on unsupported backends, rejects the same id with changed rendered content, and fails closed after the provider retention window. Direct required deliverPayload() calls also fail before provider I/O when the background-jobs database connection is already inside a caller-owned transaction, because an outer rollback could erase the first-attempt marker. Generic SMTP remains at-least-once. Velocious includes a dedicated ResendSmtpMailerBackend for Resend's 24-hour Resend-Idempotency-Key contract; see Mailers for setup, expiry, reconciliation, and non-exactly-once guarantees.

Build the rendered payload without sending when the app needs to store an audit copy or hand delivery to its own transport:

const payload = await new TasksMailer().newNotification(task, user).buildPayload()

If your mailer needs async setup, keep the action sync and pass actionPromise:

resetPassword(user) {
  return this.mail({
    to: user.email(),
    subject: "Reset your password",
    actionPromise: (async () => {
      this.token = await user.resetToken()
      this.assignView({user, token: this.token})
    })()
  })
}

Configure a delivery handler for non-test environments:

setDeliveryHandler(async ({to, subject, html}) => {
  // send the email via your provider
})

Mailer backends can also be configured via your app configuration.

import {SmtpMailerBackend} from "velocious/build/src/mailer.js"

export default new Configuration({
  mailerBackend: new SmtpMailerBackend({
    connectionOptions: {
      host: "smtp.example.com",
      port: 587,
      secure: false,
      auth: {user: "smtp-user", pass: "smtp-pass"}
    },
    defaultFrom: "[email protected]"
  })
})

Install the SMTP peer dependency in your app:

npm install smtp-connection

When connectionOptions.auth is present, the SMTP backend authenticates before sending the message.

Test deliveries are stored in memory:

const sent = deliveries()

Translations

Velocious uses gettext-universal by default. Configure your locales and fallbacks in the app configuration:

export default new Configuration({
  locale: () => "en",
  locales: ["en"],
  localeFallbacks: {en: ["en"]}
})

Load compiled translations for gettext-universal (for example, JS files generated from .po files):

import gettextConfig from "gettext-universal/build/src/config.js"
import en from "./locales/en.js"

Object.assign(gettextConfig.getLocales(), {en})

Use translations in mailer views with _:

<b><%= _("Hello %{userName}", {userName}) %></b>

If you want a different translation backend, set a custom translator:

configuration.setTranslator((msgID, args) => {
  // return translated string
})

Models

npx velocious g:model Account
npx velocious g:model Task

Frontend models from backend resources

You can generate lightweight frontend model classes from resource definitions in your configuration.

import FrontendModelBaseResource from "velocious/build/src/frontend-model-resource/base-resource.js"

class UserResource extends FrontendModelBaseResource {
  static resourceConfig() {
    return {
      attributes: ["id", "name", "email"],
      relationships: {
        projects: {type: "hasMany", model: "Project"}
      }
    }
  }
}

export default new Configuration({
  // ...
  backendProjects: [
    {
      path: "/path/to/backend-project",
      frontendModels: {
        User: UserResource
      }
    }
  ]
})

frontendModels entries must be FrontendModelBaseResource subclasses. Built-in CRUD/find/index/serialize behavior lives in the base class, and app resources override only the pieces they actually need. Resource-level index customization should prefer indexQuery() or the pagination/search/sort hooks over replacing records(), so built-in pluck and aggregate count support can keep using the same query. See docs/frontend-model-resources.md for the resource extension points.

Custom class- and instance-level commands are declared via collectionCommands / memberCommands. Each entry is a plain camelCase method name, or a {name, args?, returnType?} object that types the command's arguments and response — e.g. memberCommands: ["suspend", {name: "refresh", args: [{name: "age", type: "number"}], returnType: "string"}]. Plain string commands derive args and return types from the backend resource method JSDoc; shared DTO @import types outside the backend src tree are preserved in generated frontend models, while backend-local helper types such as ReturnType<typeof serializePayload> are rejected. A command whose args are a single object literal with only optional fields generates an omittable parameter (record.command() works without passing {}); any required field keeps the argument mandatory. See docs/frontend-model-resources.md#custom-commands.

Resources expose the full CRUD ability set (create, destroy, read, update) by default. To restrict the API surface — for example to a read-only resource — declare an explicit subset:

class AuditLogResource extends FrontendModelBaseResource {
  static abilities = ["read"]
  static attributes = ["id", "message", "createdAt"]
}

Generate classes:

npx velocious g:frontend-models

Frontend-model attributes can usually be declared by name. The generator infers JSDoc typedefs and nullability from backend model columns and translated attribute columns. When an attribute entry needs resource-specific options such as selectedByDefault: false, keep only that option in the resource config, for example {name: "archivedAt", selectedByDefault: false}; the column type and nullability are still inferred. For computed resource attributes, add a typed ${attributeName}Attribute(model) method with an @returns tag in the backend project's src tree. Resource attribute return types take precedence over column types because the resource method controls the serialized value. If Velocious cannot infer a read attribute from a column, generated model accessor, resource method JSDoc, or explicit metadata, generation fails with a clear error instead of emitting a broad fallback type.

This creates src/frontend-models/user.js (and one file per configured resource). Import each model directly by its file path (e.g. import User from ".../frontend-models/user.js"); src/frontend-models/setup.js side-effect-imports every model file so they self-register (import it once at app startup). No barrel/index.js is generated. Every generated file — the per-model files and setup.js here, and the base-model files from g:base-models — starts with an auto-generated banner stating it must not be edited manually because changes are overwritten on the next regeneration, and naming the command that regenerates it. Apply changes at their source (resource/model definitions or the generator) and regenerate. Generated classes support:

  • await User.find(5)
  • await User.findBy({email: "[email protected]"})
  • await User.findByOrFail({email: "[email protected]"})
  • await User.toArray()
  • await User.create({name: "John"})
  • await Task.sort("-createdAt").toArray()
  • await Task.order("-createdAt").toArray()
  • await Task.limit(10).offset(20).toArray()
  • await Task.page(2).perPage(25).toArray()
  • await Task.where({project: {creatingUser: {reference: "owner-b"}}}).toArray()
  • await Task.joins({project: {creatingUser: true}}).where({project: {creatingUser: {reference: "owner-b"}}}).toArray()
  • await Task.sort({project: {creatingUser: ["reference", "desc"]}}).toArray()
  • await Task.sort({project: {account: [["name", "desc"], ["createdAt", "asc"]]}}).toArray()
  • await Task.group({project: {account: ["id"]}}).toArray()
  • await Task.sort({comments: ["body", "asc"]}).distinct().toArray()
  • await Task.count()
  • await Task.pluck("id")
  • await Task.pluck({project: ["id"]})
  • await User.preload({projects: ["tasks"]}).toArray()
  • await Task.load()
  • await Project .preload(["tasks"]) .select({Project: ["id", "createdAt"], Task: ["updatedAt"]}) .toArray()
  • await user.update({...})
  • await user.save() (persists new records and updates existing records; also carries dirty nested children through the single request when the parent opts in — see docs/nested-attributes.md)
  • await user.destroy()
  • user.markForDestruction() to queue a loaded child for destruction on the next parent save (see docs/nested-attributes.md)
  • State helpers like user.isNewRecord(), user.isPersisted(), user.isChanged(), and user.changes()
  • Attribute methods like user.name() and user.setName(...)
  • Relationship helpers (when relationships are configured), for example task.project(), await task.projectOrLoad(), await project.tasks().toArray(), await project.tasks().load(), and project.tasks().build({...})
  • Preload relationships onto records you already have with await record.preload(Model.preload({...}).select({...})) (or Preloader.preload(records, ...) for arrays), including selectsExtra(...) and a {force: true} reload option — see docs/frontend-models.md
  • Attachment helpers (when attachments are configured), for example await task.descriptionFile().attach(file), await task.descriptionFile().download(), await task.files().purgeAll(), and await task.update({descriptionFile: file})

React components can subscribe to lifecycle broadcasts without manual cleanup code:

import useModelClassEvent from "velocious/build/src/frontend-models/use-model-class-event.js"

useModelClassEvent(Subscription, ["create", "update"], () => {
  void loadSubscriptionStatus()
})

useCreatedEvent, useUpdatedEvent, and useDestroyedEvent are also available. useUpdatedEvent and useDestroyedEvent accept either a model class or model instance. Lifecycle subscriptions accept the same projection options as frontend-model queries for event records, including select, preload, withCount, abilities, and queryData.

Frontend-model group(...) is attribute/path based and does not accept raw SQL fragments. Use model/relationship shapes (for example Task.group({project: {account: ["id"]}})) so grouping resolves through known relationships and mapped columns. Frontend-model where(...) supports nested relationship descriptors (for example Task.where({project: {creatingUser: {reference: "owner-b"}}})) and does not accept raw SQL fragments. Frontend-model joins(...) supports relationship-object descriptors only (for example Task.joins({project: {creatingUser: true}})) and rejects raw SQL join strings. Frontend-model distinct(...) only accepts booleans (true by default) and is applied server-side through the backend query API. Frontend-model pluck(...) validates attribute/path descriptors against configured resource/model metadata and does not accept SQL fragments or hidden raw model columns when the resource declares an explicit attribute list. Frontend-model query fields are limited to attributes exposed by the backend resource. Use {name: "attributeName", selectedByDefault: false} for fields that may be selected or filtered explicitly but should stay out of default payloads.

When backend payloads include __preloadedRelationships, nested frontend-model relationships are hydrated recursively. Relationship methods can use getRelationshipByName("relationship").loaded() and will throw when a relationship was not preloaded.

When queries include select(...), backend frontend-model actions only serialize selected attributes for each model class. Reading a non-selected attribute on a frontend model raises AttributeNotSelectedError.

You do not need to manually define frontend-index / frontend-find / frontend-create / frontend-update / frontend-destroy routes for those resources. Velocious can auto-resolve frontend model command paths from backendProjects.frontendModels.

For backend models, you can declare attachment helpers directly:

Task.hasManyAttachments("files")
Task.hasOneAttachment("descriptionFile")
Task.hasOneAttachment("archivedPdf", {driver: "s3"})

See Backend record attachments for the complete input, storage-driver, lifecycle, and path-security contracts.

You can also pass a driver class or instance directly on the attachment:

import NativeDriver from "./storage/native-driver.js"

Task.hasOneAttachment("mobileCache", {driver: NativeDriver})
// or:
Task.hasOneAttachment("mobileCache", {driver: new NativeDriver()})

Then use them from backend records:

await task.descriptionFile().attach({
  content: "my file content",
  filename: "file.doc"
})
await task.archivedPdf().attach({
  path: "/var/app/uploads/archive.pdf",
  contentType: "application/pdf"
})
const descriptionFileUrl = await task.descriptionFile().url()
await task.update({
  descriptionFile: {
    contentBase64: Buffer.from("my file content").toString("base64"),
    filename: "my-doc.doc"
  }
})

Purge a record's attachments — both the stored files and their rows — for example before destroying the owner record:

const purgedCount = await task.files().purgeAll()

purgeAll() deletes each attachment's backing storage and then its row, and removes only the attachments that existed when the purge started (a concurrent attach() for the same record/name is left intact). It throws without deleting anything if a storage driver has no delete operation, so a driver configured without deletion can never silently leak storage. It is a no-op for unpersisted records and returns the number of attachments purged.

Configure attachment storage drivers in Configuration:

export default new Configuration({
  attachments: {
    defaultDriver: "filesystem",
    // Path-based attachment input is disabled by default.
    // Enable explicitly only when backend-side file ingestion is needed.
    allowPathInput: false,
    // Optional allowlist when allowPathInput is true.
    allowedPathPrefixes: ["/var/app/uploads"],
    drivers: {
      filesystem: {
        directory: "/tmp/velocious-attachments"
      },
      native: {
        write: async ({attachmentId, contentBase64, filename}) => {
          // Persist using your native file API and return a storage key
          return {storageKey: `${attachmentId}-${filename}`}
        },
        read: async ({storageKey}) => {
          // Return Buffer, Uint8Array, ArrayBuffer or base64 string
          return await readNativeFile(storageKey)
        },
        url: async ({storageKey}) => {
          return `file://${storageKey}`
        }
      },
      s3: {
        bucket: "my-bucket",
        region: "eu-west-1",
        signedUrlExpiresIn: 3600
      }
    }
  }
})

If you want backend-side path ingestion, enable it explicitly:

new Configuration({
  attachments: {
    allowPathInput: true,
    allowedPathPrefixes: ["/var/app/uploads"]
  }
})

Then {path: "..."} inputs are only accepted when the path resolves inside one of the allowed prefixes and the once-opened handle identifies a regular file. Its exact byte size comes from that handle's stat snapshot. The filesystem driver copies it with a bounded, backpressured stream and the S3 driver sends a Node Readable; on current nullable schemas neither driver first materializes the whole file as a Buffer or Base64 value. Legacy schemas with a non-null content_base64 column instead materialize the opened snapshot once before driver persistence and reuse those exact bytes for storage and the database Base64. Path replacement cannot switch the opened source, truncation is rejected, later appends are ignored, and the source is closed after persistence.

The native driver's documented write({contentBase64, ...}) callback remains source-compatible. For path input only, that driver reads and Base64-encodes the opened source after driver selection on current schemas; legacy path input arrives pre-materialized with the same Base64 written to the database. Existing Buffer, string, browser, and UploadedFile inputs keep their in-memory behavior. See docs/attachments.md for the normalized input passed to custom drivers.

For frontend models, configure resourceConfig().attachments and use:

await frontendTask.update({descriptionFile: file})
const descriptionFile = await frontendTask.descriptionFile().download()
const descriptionFileUrl = await frontendTask.descriptionFile().url()
const descriptionFileMetadata = await frontendTask.descriptionFile().first()
const filesMetadata = await frontendTask.files().toArray()
await frontendTask.attach(file)

Frontend model attachment input does not support {path: ...}. Use File/Blob/bytes/contentBase64 payloads instead. Attachment metadata is exposed through the built-in VelociousAttachment frontend model with safe fields only: id, recordType, recordId, name, position, filename, contentType, byteSize, createdAt, and updatedAt. Storage internals such as driver, storageKey, and contentBase64 remain hidden and non-queryable. Direct metadata queries require owner filters: recordType, recordId, and name.

When your frontend app calls a backend on another host/port (or under a path prefix), configure transport once:

import FrontendModelBase from "velocious/build/src/frontend-models/base.js"

FrontendModelBase.configureTransport({
  requestContext: () => ({projectId: currentProject.id, routingEpoch: currentProject.routingEpoch}),
  url: "http://127.0.0.1:4501/frontend-models",
  timeZone: () => Intl.DateTimeFormat().resolvedOptions().timeZone
})

Available transport options:

  • url (can also be a relative path like "/frontend-models" on web)
  • requestContext (a scalar plain object or synchronous function returning one) captures immutable remote tenant-routing params independently for each CRUD/custom command and event subscription. See docs/remote-request-context.md.
  • timeZone (an IANA timezone string or a function returning one). Browser clients auto-detect this when it is not configured. Frontend-model datetime strings without an explicit timezone are interpreted in this request timezone and stored/queried as UTC instants.
  • timeout (milliseconds or a function returning milliseconds) bounds each request, while signal (an AbortSignal or a function returning one) supports caller cancellation. See docs/frontend-models.md for timeout and cancellation behavior.

Use await FrontendModelBase.waitForIdle() when a test harness or app lifecycle needs to wait for queued, scheduled, and active frontend-model transport requests to finish before resetting state.

Frontend-model HTTP requests always use credentials: "include" so shared custom commands can set session cookies without app-level transport overrides.

Unexpected frontend-model endpoint failures return their original message and full stack trace by default in every environment, including production. Responses use errorType: "internal_error", a server-generated correlationId shared with the matching framework-error report, and the established debugErrorClass, debugErrorMessage, and debugBacktrace fields. Expected application failures can use VelociousError.safe(message, {errorType, details, code}); generated frontend-model callers preserve the server's safe error fields without adding irrelevant debug fields. See docs/frontend-models.md. Invalid client query descriptors, such as unknown select, where, search, joins, preload, group, sort, pluck, or Ransack attributes, return the specific frontend-model query error message with velocious.code: "frontend-model-query-error" and are not emitted as framework errors. Invalid frontend-model write attributes and attachment names, including attributes rejected by permittedParams(), return the specific safe error message with velocious.code: "frontend-model-attribute-error" and are not emitted as framework errors. To mask unexpected internal details, explicitly opt out for the application configuration:

const configuration = new Configuration({
  exposeInternalErrorsToClients: false
})

With this opt-out, built-in commands, custom commands, and sync replay failures return errorMessage: "Request failed." and omit the debug message and stack fields in every environment. secureFrontendModelErrors: true remains a deprecated compatibility alias when exposeInternalErrorsToClients is omitted; an explicit exposeInternalErrorsToClients value always wins.

Backends can append client-safe metadata to frontend-model error responses with configuration.addClientErrorPayloadReporter(...). Reporters receive the caught error, the current request, a safe requestDetails snapshot, and a small context object, and should only return fields that are safe for clients to see. Frontend-model endpoint failures include context.frontendModelEndpoint, action, commandType, model, requestId, and expectedError. When exposure is disabled, Velocious strips the established debug fields even if a reporter supplies them. This is useful for attaching an error-reporting URL while keeping an opted-out error message generic:

configuration.addClientErrorPayloadReporter(async ({error, requestDetails, context}) => {
  const report = await reportErrorToService({error, requestDetails, context})

  return {bugReportUrl: report.url}
})

requestDetails includes httpMethod, path, and a parsed body snapshot when available. The body snapshot redacts common secret keys, truncates large strings and arrays, summarizes uploaded files and buffers without bytes, and compacts oversized frontend-model batches while preserving requestId, model, commandType / customPath, and payload shape.

For sqlite web databases, Velocious automatically picks the best browser persistence backend it can use: OPFS when a smoke test succeeds, then IndexedDB. Existing persisted bytes in a worse backend are migrated into the selected backend when possible. If neither OPFS nor IndexedDB is usable, Velocious keeps the legacy localStorage-style backend as a compatibility fallback. See docs/sqlite-web-persistence.md for the backend selection details.

Velocious defaults to https://sql.js.org/dist/<file> for sql.js wasm loading. You can override wasm resolution per database config with locateFile:

import SqliteDriver from "velocious/build/src/database/drivers/sqlite/index.web.js"

export default new Configuration({
  database: {
    test: {
      default: {
        driver: SqliteDriver,
        type: "sqlite",
        name: "app-db",
        locateFile: (file) => `/assets/sqljs/${file}`
      }
    }
  }
})

If you want to serve sql.js assets directly from your running Velocious backend, install the built-in sql.js asset route plugin and point locateFile to it:

import installSqlJsWasmRoute, {sqlJsLocateFileFromBackend} from "velocious/build/src/plugins/sqljs-wasm-route.js"
import SqliteDriver from "velocious/build/src/database/drivers/sqlite/index.web.js"

const configuration = new Configuration({
  // ...
  database: {
    development: {
      default: {
        driver: SqliteDriver,
        type: "sqlite",
        name: "app-db",
        locateFile: sqlJsLocateFileFromBackend({
          backendBaseUrl: "http://127.0.0.1:4501",
          routePrefix: "/velocious/sqljs"
        })
      }
    }
  }
})

installSqlJsWasmRoute({
  configuration,
  routePrefix: "/velocious/sqljs"
})

Frontend-model command transport preserves Date and undefined by encoding them as marker objects in JSON and decoding them on the other side:

  • Date -> {__velocious_type: "date", value: "<ISO string>"}
  • undefined -> {__velocious_type: "undefined"}
  • bigint -> {__velocious_type: "bigint", value: "<decimal string>"}
  • NaN / Infinity / -Infinity -> {__velocious_type: "number", value: "NaN" | "Infinity" | "-Infinity"}

Frontend-model commands raise an Error when the backend responds with {status: "error"} (using errorMessage when present), so unauthorized or missing-record update/find/destroy responses fail fast in frontend code.

Route resolver hooks

Libraries can hook unresolved routes and hijack them before Velocious falls back to the built-in 404 controller.

export default new Configuration({
  // ...
  routeResolverHooks: [
    ({currentPath}) => {
      if (currentPath !== "/special-route") return null

      return {controller: "hijacked", action: "index"}
    }
  ]
})

Hook return value:

  • null to skip
  • {controller, action} to resolve the request
  • Optional controllerClass to resolve without importing a controller path
  • Optional params object to merge into request params
  • Optional controllerPath string to resolve a controller file outside the app route directory
  • Optional viewPath string override for view rendering lookups

Plugin routes helper

For plugin-style integrations, you can register routes with a simple DSL:

configuration.routes((routes) => {
  routes.get("/velocious/sqljs/:sqlJsAssetFileName", {
    to: [SqlJsController, "downloadSqlJs"]
  })
})

Supported route helpers:

  • routes.get(path, {to: [ControllerClass, "action"], params?})
  • routes.post(path, {to: [ControllerClass, "action"], params?})

Rampway deployment control plane

Applications can install rampway@^0.4.0 and mount its package-owned Velocious control plane through the existing routes DSL. Keep bearer tokens in backend secrets and provide explicit allowlisted config paths and release branches:

npm install rampway@^0.4.0 velocious@^1.0.577

Rampway 0.4.0 declares velocious ^1.0.574 as its peer range, but applications mounting this API must use Velocious 1.0.577 or newer. Versions 1.0.574 through 1.0.576 attempted an app-local controller import before the package-supplied controllerClass, allowing a same-named app controller to shadow Rampway's authenticated controller.

import RampwayDeploymentApi from "rampway/velocious"
import deploymentSecrets from "./secrets/deployments.js"

routes.draw((route) => {
  route.mount(RampwayDeploymentApi, {
    accessTokens: deploymentSecrets.rampwayAccessTokens,
    at: "/rampway/deployments",
    projects: {
      "my-app": {
        stages: {
          production: {
            configPath: "/srv/my-app/control/rampway.config.mjs",
            releaseBranch: "main"
          }
        }
      }
    },
    workerBootstrapPath: "/srv/my-app/control/rampway-velocious-worker.mjs"
  })
})

Rampway owns authentication, deployment execution, idempotency, durable runs, audits, reconciliation, and the detached worker. Velocious supplies its normal route, request, error-event, and database abstractions. See docs/rampway-integration.md for bootstrap, persistence, security, and rollback requirements.

import Record from "velocious/build/src/database/record/index.js"

class Task extends Record {
}

Task.belongsTo("account")
Task.translates("description", "subTitle", "title")
Task.validates("name", {presence: true, uniqueness: true})

export default Task

Generated belongs-to setters synchronize the loaded relationship and foreign key before save, so task.setProject(project) updates task.projectId(), task.changes(), callbacks, and scoped features such as actsAsList. Custom relationship primary keys are also used after autosaving an assigned new or dirty related record. Generated backend write attributes accept belongs-to relationship names for create and update payloads. See docs/relationships.md.

Translated models also get a currentTranslation hasOne relationship scoped to the first available row in the current locale fallback order. See docs/translations.md for preloading and frontend-model sorting behavior.

Async class APIs initialize record metadata on first use when a model has not already been initialized eagerly. See docs/model-initialization.md for the eager and lazy initialization behavior, including atomic shared bootstrap and complete recovery after an eager initialization failure or database-connection closure without overlapping stale and current bootstrap side effects.

Lifecycle callbacks

Register lifecycle callbacks with either a function or an instance method name. Registrations run in order, so you can stack multiple callbacks on the same lifecycle hook.

class Task extends Record {
  async validateSomething() {
    await doSomethingElse()
  }
}

Task.beforeValidation(async (task) => {
  await doSomething(task)
})

Task.beforeValidation("validateSomething")

Preloading relationships

const tasks = await Task.preload({project: {translations: true}}).toArray()
const projectNames = tasks.map((task) => task.project().name())

Load a relationship after init

const task = await Task.find(5)

const project = await task.projectOrLoad()

await task.loadProject()

const sameProject = task.project()
const project = await Project.find(4)
const tasks = await project.tasks().toArray()
const refreshedTasks = await project.tasks().load()

await project.loadTasks()

const tasks = project.tasks().loaded()

Auto-batch-preload (cohort loading)

When records are loaded as part of a batch (e.g. Task.where(...).toArray()), the first lazy access to a relationship on any sibling batch-loads that relationship for every sibling record in one query — avoiding the classic N+1.

const tasks = await Task.where({state: "open"}).toArray()

// First call issues ONE query to load the project for every task in the batch.
const firstProject = await tasks[0].projectOrLoad()

// Subsequent sibling accesses hit the preloaded cache — no extra query.
const secondProject = tasks[1].project()

Auto-load is triggered by the async access paths that already exist: model.${name}OrLoad(), model.relationshipOrLoad("..."), and model.relationship().toArray() / model.relationship().load() for hasMany. The synchronous accessor model.relationship() still throws when the relationship has not been loaded — call the async form if you want the lazy-load behavior.

Scoped queries opt out of cohort batching by design, because the filter is specific to the accessing record:

// Triggers cohort batch — all cohort siblings get their comments preloaded in one query.
await firstTask.comments().load()

// Does NOT trigger cohort — scoped filter is unique to this call.
await firstTask.comments().query().where({isResolved: true}).load()

Disable auto-load per relationship:

Task.belongsTo("project", {autoload: false})

Disable auto-load globally via the framework configuration:

new Configuration({
  autoload: false,
  // ...
})

Both flags default to true. When disabled, lazy access falls back to a per-record load.

The same cohort auto-batch-preload applies to frontend models. When a batch is loaded from the backend (Task.where(...).toArray() or similar), the first async relationship access on any cohort sibling triggers one combined HTTP request that preloads that relationship for every sibling at once:

const tasks = await Task.toArray()

// First call issues ONE request to preload the project for every task in the batch.
const firstProject = await tasks[0].projectOrLoad()

// Sibling has been populated from the same response — no extra request.
const secondProject = tasks[1].project()

The generator threads the per-relationship autoload: false flag through automatically, so Task.belongsTo("project", {autoload: false}) on the backend also disables cohort batching on the generated frontend model.

Disable auto-batch-preload globally on the frontend:

import FrontendModelBase from "velocious/frontend-models"

FrontendModelBase.setAutoload(false)

Scoped frontend queries (e.g. Task.where(...).preload([name]).toArray() from user code) bypass cohort batching by design, same as the backend. Siblings with locally set state from .setRelationship() / .build() are preserved across cohort batches.

Backend relationship build(...) / create(...) helpers and generated singular builders with a concrete target use that model's generated write-attribute type. Model-valued relationship attributes are accepted, while unknown and invalid attributes fail type checking. Targetless polymorphic belongsTo builders remain generic because no single target write contract exists. See docs/relationships.md.

Through relationships

Use the through option on hasMany to define a relationship that traverses an intermediate (join) table:

Invoice.hasMany("invoiceGroupLinks")
Invoice.hasMany("invoiceGroups", {through: "invoiceGroupLinks", className: "InvoiceGroup"})

Through relationships work with both instance-level loading and batch preloading:

// Instance-level loading
const invoice = await Invoice.find(1)
const groups = await invoice.invoiceGroups().toArray()

// Batch preloading
const invoices = await Invoice.preload({invoiceGroups: true}).toArray()
const groups = invoices[0].invoiceGroupsLoaded()

The intermediate relationship (e.g. invoiceGroupLinks) must be defined as a separate hasMany on the same model. The foreignKey option on the through relationship specifies the column on the target table that points to the intermediate table (defaults to the conventional foreign key).

Dependent relationships

dependent controls what happens to child records when a parent is destroyed:

Project.hasMany("tasks", {dependent: "restrict"})
Project.hasOne("projectDetail", {dependent: "destroy"})

dependent: "destroy" loads and destroys children before deleting the parent. For hasOne, Velocious destroys at most one matching child; when no matching child exists, the parent destroy continues without error. Polymorphic hasOne dependencies match both the foreign key and type column so another model with the same ID is not destroyed. dependent: "restrict" blocks the parent destroy when dependent rows exist.

Relationship scopes

You can pass a scope callback to hasMany, hasOne, or belongsTo to add custom filters. The callback receives the query and is also bound as this:

Project.hasMany("acceptedTasks", (scope) => scope.where({state: "accepted"}), {className: "Task"})
Project.hasOne("activeDetail", function() { return this.where({isActive: true}) }, {className: "ProjectDetail"})
Comment.belongsTo("acceptedTask", (scope) => scope.where({state: "accepted"}), {className: "Task"})

Join path table references

When joining relationships, use getTableForJoin to retrieve the table (or alias) for a join path:

const query = Task.joins({project: {account: true}})
const accountTable = query.getTableForJoin("project", "account")

Inside relationship scopes, getTableForJoin() is relative to the current scope path:

Project.hasMany("acceptedTasks", function() {
  return this.where(`${this.getTableForJoin()}.state = 'accepted'`)
}, {className: "Task"})

Model scopes

Backend records and frontend models can define reusable named scopes with defineScope(...).

class Task extends TaskBase {
  static withAccepted = this.defineScope(({query}, accepted) => query.where({accepted}))
}

await Task.withAccepted(true).toArray()
await Task.where({projectId: 1}).scope(Task.withAccepted.scope(true)).toArray()
await Task.joins({project: {tasks: true}}).scope(["project", "tasks"], Task.withAccepted.scope(true)).toArray()

Model.scopeName(args...) starts a fresh query for that model. Model.scopeName.scope(args...) returns a reusable scope descriptor for .scope(...) on an existing query. Backend record queries also support .scope(path, descriptor) to apply a scope to a joined relationship path.

Backend record scopes receive alias-aware SQL context:

class Task extends TaskBase {
  static nameLike = this.defineScope(({driver, query, table}, value) => query.where(
    `${driver.quoteTable(table)}.${driver.quoteColumn("name")} LIKE ${driver.quote(`%${value}%`)}`
  ))
}

The table value is the active table reference for the current query and may be an alias from FROM ... AS ..., not just Task.tableName().

Joined-path scopes receive the joined path in context.path and may only add where(...) and joins(...) clauses.

Finding records

find() and findByOrFail() throw an error when no record is found. findBy() returns null. These apply to records.

Create records

const task = new Task({identifier: "task-4"})

task.assign({name: "New task})

await task.save()
const task = await Task.create({name: "Task 4"})

Bulk insert

Use insertMultiple to insert many rows in one call:

await Task.insertMultiple(
  ["project_id", "name", "created_at", "updated_at"],
  [
    [project.id(), "Task 1", new Date(), new Date()],
    [project.id(), "Task 2", new Date(), new Date()]
  ]
)

If a batch insert fails, you can retry each row and collect results:

const results = await Task.insertMultiple(
  ["project_id", "name"],
  [
    [project.id(), "Task A"],
    [project.id(), "Task A"]
  ],
  {retryIndividuallyOnFailure: true, returnResults: true}
)

console.log(results.succeededRows, results.failedRows, results.errors)

Large batches are split into multiple INSERT ... VALUES statements so each statement stays within database limits. Two database-configuration keys control the splitting:

  • maxRowsPerInsert — maximum rows per statement (default: 500).
  • maxInsertSqlBytes — maximum serialized SQL size in bytes per statement (default: 1048576, i.e. 1 MiB).

A new chunk is started when the next row would exceed either limit. Row order is preserved across chunks.

Important: when insertMultiple is called outside a transaction, each chunk commits independently. If a later chunk fails, earlier chunks remain persisted. Wrap the call in a transaction when you need all-or-nothing semantics:

await Task.transaction(async () => {
  await Task.inser