> ## Documentation Index
> Fetch the complete documentation index at: https://www.offlineprotocol.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Configuration

> All configuration interfaces for initializing and tuning the Mesh SDK protocol.

# Configuration

## ProtocolConfig

Only two fields are required: `appId` and `profile`.

```typescript theme={null}
interface ProtocolConfig {
  appId: string;
  profile: string;
  transports?: TransportsConfig;
  binaryWireEnabled?: boolean;   // default: true
  encryption?: EncryptionConfig;
  group?: GroupConfig;
  dors?: DorsConfig;
  relay?: RelayConfig;
  network?: NetworkConfig;
  reliability?: ReliabilityConfig;
  fileTransfer?: FileTransferConfig;
  path?: PathConfig;
}
```

<Warning>
  **`userId` was removed in v0.21.0.** It is replaced by `profile`, which is *not* the same
  field renamed. `profile` selects which stored identity this instance runs as and never
  leaves the device. Your identity on the wire is a self-certifying `off1…` address the SDK
  derives from an identity key it mints for itself. Read it with
  [`localAddress()`](/docs/mesh-sdk/methods#identity) or the
  [`identity_ready`](/docs/mesh-sdk/events#identity_ready) event. An app cannot choose it.

  See [Identity & Addressing](/docs/mesh-sdk/identity) for the migration.
</Warning>

### appId

Application identifier. Combined with `profile` it forms the storage namespace.

### profile

Local profile selector: which stored identity this instance runs as.

The storage namespace is `SHA-256(domain ‖ 0x00 ‖ appId ‖ 0x00 ‖ profile)`. An app hosting
several accounts gives each its own value; an app hosting one can pass a constant such as
`'default'`.

If you are migrating from `userId`, pass the same string through as `profile`. That keeps
you in the same storage namespace.

### binaryWireEnabled

Kill switch for the compact binary wire codec on mesh hops (default: `true`). Negotiated per
peer via the key package; decoding of inbound binary frames is always on. Disabling stops
advertising and emitting, so both directions fall back to JSON framing. It is the hop-local
sibling of `encryption.compactEnvelopeEnabled`.

## TransportsConfig

Five transports are available. BLE is on by default; the rest are opt-in.

```typescript theme={null}
interface TransportsConfig {
  ble?: BleTransportConfig;
  internet?: InternetTransportConfig;
  wifiDirect?: WifiDirectTransportConfig;   // Android only
  reticulum?: ReticulumTransportConfig;
  nostr?: NostrTransportConfig;
}

interface BleTransportConfig {
  enabled: boolean;             // required
}

interface InternetTransportConfig {
  enabled: boolean;             // required
  serverAddress?: string;       // WebSocket URL
  autoReconnect?: boolean;
  reconnectDelay?: number;      // ms
  authToken?: string;           // falls back to deviceId
}

interface WifiDirectTransportConfig {
  enabled: boolean;             // required
  deviceName?: string;
  autoAccept?: boolean;
  groupOwnerIntent?: number;    // 0-15, higher = more likely group owner
}

interface ReticulumTransportConfig {
  enabled: boolean;             // required
  daemonAddress?: string;       // default: "localhost:4242"
  autoReconnect?: boolean;      // default: true
  maxReconnectAttempts?: number; // default: 0 (infinite)
}

interface NostrTransportConfig {
  enabled: boolean;             // required
  relayUrls?: string[];         // e.g. ["wss://relay.damus.io"]
  connectionTimeout?: number;   // seconds, default: 30
  autoReconnect?: boolean;      // default: true
  reconnectDelay?: number;      // ms, default: 1000
  maxReconnectAttempts?: number; // default: 0 (infinite)
  sealingEnabled?: boolean;     // default: true
  coldContactEnabled?: boolean; // default: true
}
```

`TransportType` is `'ble' | 'internet' | 'wifiDirect' | 'reticulum' | 'nostr'`.

<Note>
  Reticulum requires external infrastructure (a running Reticulum daemon, an RNode radio, or a
  network gateway). Nostr requires at least one relay URL. Both are disabled by default for
  that reason. See [Reticulum & Nostr](/docs/mesh-sdk/reticulum-nostr).
</Note>

<Warning>
  Setting `nostr.sealingEnabled: false` puts the entire protocol envelope (both addresses, app
  id, metadata map, content type, and timestamp) in relay-readable cleartext, permanently.
  It exists only to reach pre-sealing peers. Leave it on.
</Warning>

## EncryptionConfig

Controls MLS-based end-to-end encryption. Encryption is enabled and **fail-closed** by default.

```typescript theme={null}
interface EncryptionConfig {
  enabled?: boolean;                 // default: true
  autoKeyExchange?: boolean;         // default: follows `enabled`
  storePending?: boolean;            // default: follows `enabled`
  requireEncryption?: boolean;       // default: follows `enabled`
  compactEnvelopeEnabled?: boolean;  // default: true
  richPayloadEnabled?: boolean;      // default: true
  cryptoRecoveryEnabled?: boolean;   // default: true
  pendingQueue?: PendingQueueConfig;
}

interface PendingQueueConfig {
  maxPendingPerPeer?: number;  // default: 64
  maxPendingGlobal?: number;   // default: 4096
  pendingTtlMs?: number;       // default: 1800000 (30 minutes)
  overflowPolicy?: 'drop_oldest' | 'drop_newest';  // default: 'drop_oldest'
}
```

| Field                    | Meaning                                                                                                                                                                                                  |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                | Auto-encrypt every message.                                                                                                                                                                              |
| `autoKeyExchange`        | Exchange key packages on peer discovery.                                                                                                                                                                 |
| `storePending`           | Queue messages until the secure session is ready.                                                                                                                                                        |
| `requireEncryption`      | **Fail closed.** A send that cannot be encrypted fails with a typed error rather than silently falling back to plaintext. Inbound legacy plaintext media is rejected.                                    |
| `compactEnvelopeEnabled` | Kill switch for the compact MLS envelope. Negotiated per recipient; inbound parsing is always on.                                                                                                        |
| `richPayloadEnabled`     | Kill switch for the sealed rich payload: quoted replies, rich media metadata, forward attribution. Disabling drops rich extras; it never sends them cleartext.                                           |
| `cryptoRecoveryEnabled`  | 1:1 MLS crypto-failure recovery. An undecryptable message is not delivery-ACKed, so the sender's resend can deliver. Only an epoch mismatch triggers a rate-limited session re-key (1 per peer per 30s). |

<Warning>
  `requireEncryption` defaults to **true**. To deliberately operate in plaintext (an open
  broadcast mesh with no provisioned key storage, say) you must opt out explicitly. Every
  plaintext send then emits a [`security_warning`](/docs/mesh-sdk/events#security_warning) event
  with the `PLAINTEXT_SEND` reason code, once per peer.
</Warning>

## GroupConfig

```typescript theme={null}
interface GroupConfig {
  maxGroupMembers?: number;        // default: 256
  relayEnabled?: boolean;          // default: true
  relayBroadcastEnabled?: boolean; // default: true
  enforceAdminCommits?: boolean;   // default: false
}
```

`relayEnabled` controls whether groups register with the relay server. Registration is what
invite links resolve against, so leave it on unless the app never uses relay group features.

`relayBroadcastEnabled` lets a relay-synced group send one O(1) relay broadcast instead of
per-member fan-out. The flag alone never selects the broadcast: the connected relay must also
advertise the `group_delivery_v3` capability, whose settled per-recipient delivery report is
what gives the broadcast a delivery contract. Members the relay did not reach are re-sent
per-member automatically. See
[`group_message_delivery_report`](/docs/mesh-sdk/events#group_message_delivery_report).

<Warning>
  `enforceAdminCommits` is a decision about partition risk, not a hardening tweak. Leaving it
  off does not mean unauthorized changes go unnoticed; they are applied and reported via
  [`group_unauthorized_membership_change`](/docs/mesh-sdk/events#group_unauthorized_membership_change).
  Turning it on means refusing the MLS merge, so this device's epoch stays behind every member
  that accepted it, and MLS cannot heal that: the app has to re-invite. Enable it only for a
  closed deployment that controls role distribution, and never on part of a fleet.
</Warning>

## DorsConfig

Controls transport switching behavior. Can also be changed at runtime with
[`updateDorsConfig()`](/docs/mesh-sdk/methods#dors).

```typescript theme={null}
interface DorsConfig {
  preferOnline?: boolean;              // default: false
  switchHysteresis?: number;           // default: 15.0
  switchCooldownSecs?: number;         // default: 20
  bleToWifiRetryThreshold?: number;    // default: 2
  minSuccessRateBeforeEscalation?: number; // default: 0.3
  minBleSamplesBeforeSuccessRateEscalation?: number; // default: 5
  rssiSwitchThreshold?: number;        // default: -85 dBm
  congestionQueueThreshold?: number;   // default: 50
  stabilityWindowSecs?: number;        // default: 8
  poorSignalDurationSecs?: number;     // default: 10
  ttlEscalationThreshold?: number;     // default: 2
  congestionDurationSecs?: number;     // default: 10
  ttlEscalationHoldSecs?: number;      // default: 20
  historyWindowSize?: number;          // default: 10 (clamped to 1-100)
  queueRecoveryRatio?: number;         // default: 0.5 (clamped to 0-1)
}
```

## RelayConfig

```typescript theme={null}
interface RelayConfig {
  allowRelay?: boolean;          // Allow device to act as relay
  minBatteryForRelay?: number;   // Minimum battery for relaying
  relayThreshold?: number;       // Connection threshold for relay promotion
  relayPriority?: 'never' | 'auto' | 'always';
}
```

<Note>
  `relayPriority` uses `'never' | 'auto' | 'always'` here, but the runtime methods
  [`setRelayPriority()`/`getRelayPriority()`](/docs/mesh-sdk/methods#battery--relay-role) use
  `'low' | 'medium' | 'high'`. The SDK normalizes config values when applying them:
  `never → low`, `auto → medium`, `always → high`.
</Note>

## NetworkConfig

```typescript theme={null}
interface NetworkConfig {
  initialTtl?: number;  // default: 8
}
```

## ReliabilityConfig

```typescript theme={null}
interface ReliabilityConfig {
  ack?: AckConfig;
  retry?: RetryConfig;
  dedup?: DedupConfig;
}

interface AckConfig {
  defaultTimeoutMs?: number;   // default: 5000
  maxPendingAcks?: number;     // default: 1000
}

interface RetryConfig {
  maxRetries?: number;              // default: 5
  initialDelayMs?: number;          // default: 1000
  maxDelayMs?: number;              // default: 30000
  backoffMultiplier?: number;       // default: 2.0
  outboxMaxLifetimeMs?: number;     // default: 604800000 (7 days)
  pendingMessageMaxLifetimeMs?: number; // default: 7 days
}

interface DedupConfig {
  maxTrackedMessages?: number; // default: 10000
  retentionTimeSecs?: number;  // default: 3600
}
```

`outboxMaxLifetimeMs` bounds store-and-forward outbox entries. After a restart it also prunes
restored outbox entries and persisted media transfer descriptors. Expiry is terminal: a
[`message_failed`](/docs/mesh-sdk/events#message_failed) event with reason
`"Outbox lifetime exceeded"` (capacity eviction reports `"Outbox capacity exceeded"`). An
entry older than 4× this lifetime in total is dropped terminally.

`pendingMessageMaxLifetimeMs` is how long a message may wait for MLS session establishment
before a terminal `message_failed` is emitted.

<Warning>
  Zero is rejected for `maxTrackedMessages` and `retentionTimeSecs`. The three runtime updaters
  (`updateAckConfig`, `updateRetryConfig`, `updateDedupConfig`) are fallible and throw on
  invalid input.
</Warning>

## PathConfig

```typescript theme={null}
interface PathConfig {
  forwardToTopK?: number;        // default: 3
  maxCongestionLevel?: number;   // default: 0.8
}
```

## FileTransferConfig

```typescript theme={null}
interface FileTransferConfig {
  chunkSize?: number;    // default: 32768 (32KB)
  maxFileSize?: number;  // default: 104857600 (100MB)
}
```

## TelemetryConfig

Passed to [`installTelemetrySink()`](/docs/mesh-sdk/methods#telemetry) rather than to the
constructor.

```typescript theme={null}
interface TelemetryConfig {
  scrubIds?: boolean;          // default: true
  mlsVerbosity?: 'off' | 'lifecycle' | 'diagnostic';  // default: 'lifecycle'
  metricsCadenceMs?: number;   // default: 5000
  routingDiagnostic?: boolean; // default: false
  enablePollQueue?: boolean;   // default: true
  mlsSamplingBypass?: boolean; // default: false
}
```

Omitting `metricsCadenceMs` yields the default cadence. There is currently no way to disable
periodic emission through this config. Push-only integrations should pass
`enablePollQueue: false` to skip the per-emit JSON envelope build.

## Full example

```typescript theme={null}
import { OfflineProtocol } from '@offline-protocol/mesh-sdk';

const protocol = new OfflineProtocol({
  appId: 'my-app',
  profile: 'default',
  transports: {
    ble: { enabled: true },
    internet: {
      enabled: true,
      serverAddress: 'wss://relay.example.com',
      autoReconnect: true,
      authToken: 'your-auth-token',
    },
    nostr: {
      enabled: true,
      relayUrls: ['wss://relay.damus.io'],
    },
  },
  encryption: {
    enabled: true,
    autoKeyExchange: true,
    storePending: true,
    requireEncryption: true,
    pendingQueue: {
      maxPendingPerPeer: 64,
      maxPendingGlobal: 4096,
      pendingTtlMs: 1_800_000,
      overflowPolicy: 'drop_oldest',
    },
  },
  group: {
    maxGroupMembers: 256,
    relayEnabled: true,
  },
  relay: {
    allowRelay: true,
    minBatteryForRelay: 30,
    relayPriority: 'auto',
  },
});
```

<Card title="Next: API Methods" icon="arrow-right" href="/docs/mesh-sdk/methods">
  Explore all available methods for messaging, encryption, groups, and more.
</Card>
