API reference › @evolu/common › local‑first/Evolu › EvoluAppConfig

Defined in: packages/common/src/local-first/Evolu.ts:1959

Configuration for createEvoluApp. The other options apply to every AppOwner's instance.

Extends

Properties

appName

readonly appName: string & Brand<"UrlSafeString"> & Brand<"AppName">;

Defined in: packages/common/src/local-first/Evolu.ts:227

An application name used in logs and local database names.

Evolu combines appName with the AppOwner identity to identify the local database. Changing either opens a different database. Keep appName stable across ordinary application updates.

Stored instances that share an appName and AppOwner open the same database and must use the same schema. Evolu applies the schema of the first instance and later instances join it. Each memory instance has its own database; see EvoluConfig.memoryOnly.

A different app name lets you create a separate local replica for the same AppOwner—for example, to test a different SQLite implementation or index configuration, or rebuild a replica for debugging while preserving the existing database.

Evolu supports running these databases concurrently. Their local separation does not isolate synchronization: replicas synchronizing the same owners through the same relay can still exchange changes. Changing appName neither migrates existing local data nor isolates incompatible schemas.

Example

import { AppName, assertEqual } from "@evolu/common";

const appName = AppName.orThrow("MyApp");
assertEqual(appName, "MyApp");

Inherited from

EvoluConfig.appName


auth

readonly auth: EvoluAppAuth;

Defined in: packages/common/src/local-first/Evolu.ts:1962


createFirstAppOwner

readonly createFirstAppOwner: boolean;

Defined in: packages/common/src/local-first/Evolu.ts:1972

Whether the startup that creates the device database also creates the first AppOwner, so the app starts with one. It never creates one in an existing device database, even when the app deleted every AppOwner. An app that starts with a choice, such as to create or restore an AppOwner, sets it to false. To start over, add a new AppOwner first, then remove the old one with EvoluApp.remove, so a crash between the two leaves two AppOwners rather than none.


device?

readonly device?: EvoluAppDeviceConfig<D>;

Defined in: packages/common/src/local-first/Evolu.ts:1999

The app's own tables in the device database; see DeviceEvolu.


indexes?

readonly indexes?: IndexesConfig;

Defined in: packages/common/src/local-first/Evolu.ts:332

Use the indexes option to define SQLite indexes.

Unique indexes are rejected with an assertion because concurrent offline writes can violate uniqueness during synchronization. See Uniqueness.

Table and column names are not typed because Kysely doesn't support it.

https://medium.com/@JasonWyatt/squeezing-performance-from-sqlite-indexes-indexes-c4e175f3c346

Example

import { createEvolu, id, testAppName, testAppOwner } from "@evolu/common";

const Schema = {
  todo: { id: id("Todo") },
  todoCategory: { id: id("TodoCategory") },
};

const _createTodoEvolu = createEvolu(Schema, {
  appName: testAppName,
  appOwner: testAppOwner,
  transports: [],
  indexes: (create) => [
    create("todoCreatedAt").on("todo").column("createdAt"),
    create("todoCategoryCreatedAt").on("todoCategory").column("createdAt"),
  ],
});

Inherited from

EvoluConfig.indexes


onOwnerDeleted?

readonly onOwnerDeleted?: (ownerId: string & Brand<"Id"> & Brand<"OwnerId">) => void;

Defined in: packages/common/src/local-first/Evolu.ts:355

Called when Evolu.deleteOwner deletes an owner's rows from this instance's local database, whether this or another instance in any tab asked.

The owner no longer syncs in any instance of the database, so the app stops showing it here, such as by leaving a screen of its data, whichever tab deleted it. It runs once per deletion, so an owner deleted twice, such as from two tabs at once, gets two calls.

Inherited from

EvoluConfig.onOwnerDeleted


startAppOwnerId?

readonly startAppOwnerId?:
  | string & Brand<"Id"> & Brand<"OwnerId">
  | null;

Defined in: packages/common/src/local-first/Evolu.ts:1996

The AppOwner this tab starts with:

ValueStart
omittedthe AppOwner opened most recently on this device
an IDthat AppOwner if listed, otherwise none open
nullnone open

Evolu reloads tabs on its own, such as when another build of the app starts, so a tab that showed one AppOwner can come back with one another tab opened since. Omitted, the tab starts with that one, while the ID of the AppOwner it showed starts it where it was. With null, the tab opens none, such as to let the user choose without opening and syncing an AppOwner they didn't pick. An ID that isn't listed, such as of an AppOwner another tab removed, opens none either, not the one opened most recently. The start records no open time, so a later start without this option still opens the one opened most recently.

Where the ID comes from is the app's business, such as a URL fragment, which, unlike the path or query, no server sees.


transports?

readonly transports?: readonly OwnerWebSocketTransport[];

Defined in: packages/common/src/local-first/Evolu.ts:448

Transport configuration for sync and backup.

If not specified, Evolu uses the default Evolu relay. Pass one or more transports to override it with your own relays. They must be distinct, as Evolu.useOwner requires. Pass an empty array to disable sync, which is useful when sync should be configured later.

Empty transports start the instance without sync. In that case, Evolu.useOwner must be called with explicit non-empty transports to enable sync for any Owner, including the AppOwner.

Redundancy: The ideal setup uses at least two completely independent relays - for example, a home relay and a geographically separate relay. Data is sent to both relays simultaneously, providing true redundancy similar to using two independent clouds. This eliminates vendor lock-in and ensures your app continues working regardless of circumstances - whether home relay hardware fails or disappears, or a remote relay provider shuts down.

Currently supports:

  • WebSocket: Real-time bidirectional communication with relay servers

Use createOwnerWebSocketTransport to create WebSocket transport configurations with proper URL formatting and OwnerId inclusion. The OwnerId in the URL lets a relay's isOwnerAllowed admit or reject the connection, but an admitted connection can sync any owner, and anyone who knows an OwnerId can name it, so a private relay needs access control beyond the OwnerId, as Relay describes.

The default value is:

[{ type: "WebSocket", url: "wss://free.evoluhq.com/" }].

It's a testing relay that admits anyone and may delete data, so set your own relays before release, as Relay describes.

Example

import {
  assertEqual,
  createOwnerWebSocketTransport,
  RelayUrl,
  testAppOwner,
  WebSocketUrl,
  type OwnerTransport,
} from "@evolu/common";

// Use one relay.
const _singleRelay = [
  {
    type: "WebSocket",
    url: WebSocketUrl.orThrow("wss://relay1.example.com/"),
  },
] satisfies ReadonlyArray<OwnerTransport>;

// Use independent relays for redundancy.
const _redundantRelays = [
  {
    type: "WebSocket",
    url: WebSocketUrl.orThrow("wss://relay1.example.com/"),
  },
  {
    type: "WebSocket",
    url: WebSocketUrl.orThrow("wss://relay2.example.com/"),
  },
] satisfies ReadonlyArray<OwnerTransport>;

// Start local-only before authentication. After authentication, pass an
// owner-scoped transport to evolu.useOwner.
const _localOnlyBeforeAuthentication =
  [] satisfies ReadonlyArray<OwnerTransport>;

// Include the OwnerId when the relay admits owners by it.
const authenticatedRelay = [
  createOwnerWebSocketTransport({
    url: RelayUrl.orThrow("wss://relay.example.com/"),
    ownerId: testAppOwner.id,
  }),
];

assertEqual(
  authenticatedRelay[0]?.url,
  `wss://relay.example.com/?ownerId=${testAppOwner.id}`,
);

Inherited from

EvoluConfig.transports