API reference › @evolu/nodejs › NodeJsRelayConfig

Defined in: packages/nodejs/src/local-first/Relay.ts:40

Configuration for Storage, such as quota checks.

Extends

Properties

isOwnerAllowed?

readonly optional isOwnerAllowed?: (ownerId: string & Brand<"Id"> & Brand<"OwnerId">, options: {
  signal: AbortSignal;
}) => Awaitable<boolean>;

Defined in: packages/common/dist/src/local-first/Relay.d.ts:101

Optional callback to check if an OwnerId is allowed to access the relay. If this callback is not provided, all owners are allowed.

The callback receives the OwnerId and an options object with an abort AbortSignal, and returns a Awaitable boolean: true to allow access, or false to deny.

The callback can be synchronous (for SQLite or in-memory checks) or asynchronous (for calling remote APIs).

The callback returns a boolean rather than an error type because error handling and logging are the responsibility of the callback implementation.

OwnerId is used rather than short-lived tokens because this only controls relay access, not write permissions. Since all data is encrypted on the relay, OwnerId exposure is safe.

Owners specify which relays to connect to via OwnerTransport. In WebSocket-based implementations, this check occurs before accepting the connection, with the OwnerId typically extracted from the URL query string (e.g., ws://localhost:4000?ownerId=...). The relay requires the URL to be in the correct format for OwnerId extraction.

Example

import {
  AppName,
  assertEqual,
  assertTrue,
  assertType,
  createAppOwner,
  createEvolu,
  createOwnerWebSocketTransport,
  createOwnerSecret,
  createRandomBytes,
  id,
  type AnyTask,
} from "@evolu/common";
import type { RelayConfig } from "@evolu/common/local-first";

// Create once, persist the mnemonic securely, and restore it on later runs.
const appOwner = createAppOwner(
  createOwnerSecret({ randomBytes: createRandomBytes() }),
);
// Client: include the OwnerId so the relay can authenticate the connection.
const transport = createOwnerWebSocketTransport({
  url: "wss://relay.evolu.dev",
  ownerId: appOwner.id,
});

const createTodoEvolu = createEvolu(
  { todo: { id: id("Todo") } },
  {
    appName: AppName.orThrow("AuthenticatedRelayExample"),
    appOwner,
    transports: [transport],
  },
);
assertType<typeof createTodoEvolu extends AnyTask ? true : false, true>();

// Relay: accept owners allowed by the app's access policy.
type IsOwnerAllowed = NonNullable<RelayConfig["isOwnerAllowed"]>;
const allowedOwnerIds = new Set([appOwner.id]);
const isOwnerAllowed: IsOwnerAllowed = (ownerId, { signal }) =>
  !signal.aborted && allowedOwnerIds.has(ownerId);

assertEqual(
  await isOwnerAllowed(appOwner.id, {
    signal: new AbortController().signal,
  }),
  true,
);
assertTrue(transport.url.includes(`ownerId=${appOwner.id}`));

Inherited from

RelayConfig.isOwnerAllowed


isOwnerWithinQuota

readonly isOwnerWithinQuota: (ownerId: string & Brand<"Id"> & Brand<"OwnerId">, requiredBytes: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative">) => Awaitable<boolean>;

Defined in: packages/common/dist/src/local-first/Storage.d.ts:73

Callback called before an attempt to write, to check if an OwnerId has sufficient quota for the write.

The callback receives the OwnerId and the total bytes that would be stored after the write (current stored bytes plus incoming bytes), and returns a Awaitable boolean: true to allow the write, or false to deny it due to quota limits.

The callback can be synchronous (for SQLite or in-memory checks) or asynchronous (for calling remote APIs).

The callback returns a boolean rather than an error because error handling and logging are the responsibility of the callback implementation.

Relay deployments configure this callback. Client applications observe a denied relay write as a ProtocolQuotaError in the failure of that relay's route, which syncStateToOwnerSyncStatus reports as the owner's Error status.

Example

import {
  assertEqual,
  createAppOwner,
  createOwnerSecret,
  createRandomBytes,
  PositiveInt,
} from "@evolu/common";
import type { StorageConfig } from "@evolu/common/local-first";

// Create once, persist the mnemonic securely, and restore it on later runs.
const appOwner = createAppOwner(
  createOwnerSecret({ randomBytes: createRandomBytes() }),
);
const maxBytes = PositiveInt.orThrow(1024);
const relayStorageConfig: StorageConfig = {
  isOwnerWithinQuota: (ownerId, requiredBytes) =>
    ownerId === appOwner.id && requiredBytes <= maxBytes,
};

assertEqual(
  await relayStorageConfig.isOwnerWithinQuota(
    appOwner.id,
    PositiveInt.orThrow(2048),
  ),
  false,
);

Inherited from

RelayConfig.isOwnerWithinQuota


name?

readonly optional name?: string & Brand<"UrlSafeString"> & Brand<"Name">;

Defined in: packages/common/dist/src/local-first/Relay.d.ts:19

The relay name.

Implementations can use this for identification purposes (e.g., database file name, logging).

Inherited from

RelayConfig.name


pingInterval?

readonly optional pingInterval?: PositiveDuration;

Defined in: packages/nodejs/src/local-first/Relay.ts:65

How often the relay pings every connection with a WebSocket ping frame, which browsers answer automatically. A connection from which nothing has arrived since the previous ping is terminated, so an idle dead path is detected within two intervals. Any incoming data counts, so a slow upload that delays the answer keeps its connection. A connection with data still queued in the relay for it, such as a large reply on a slow link, is not pinged or terminated until the operating system takes the data, because the answer waits behind it; TCP ends such a connection if its path is dead. A connection is terminated instead of queuing a broadcast that would leave more than 16 MB of broadcasts unsent to it, whether its client stopped reading or reads more slowly than its owners' traffic arrives; the client reconciles after it reconnects. Replies to a client's own requests do not count, because their total follows its requests. Data the operating system already holds still delays the answer, so a link too slow to send it within an interval reconnects once, after the data ahead of the ping arrives. The traffic also keeps NAT mappings alive. Defaults to thirty seconds.


port?

readonly optional port?: number & Brand<"NonNaN"> & Brand<"Finite"> & Brand<"Int"> & Brand<"NonNegative"> & Brand<"LessThanOrEqualTo65535"> & Brand<"Port">;

Defined in: packages/nodejs/src/local-first/Relay.ts:45

The HTTP server's Port. Zero requests an automatically assigned port.