Skip to main content

Tenant documents

A Tenant is a customer's identity. Everything that belongs to one is a document of its own, namespaced by metadata.tenant.

Where these documents live carries no meaning — see Documents and paths. Keys are camelCase throughout, and a misspelt one is a startup error.

Tenant

configVersion: lyno/v1
kind: Tenant
metadata:
name: acme
spec:
description: Acme Industries
domains: [acme.pbx.example.com]
dids:
numbers: ["+31201234567"]
patterns: ['^\+312012345\d{2}$']
callerId:
number: "+31201234567"
name: Acme
timezone: Europe/Amsterdam
interTenant: [globex]
moh: {...}
featureCodes: {...}
dataDir: /srv/acme-data
recording: {...}
storage: {...}
voicemail: {...}
smtp: {...}
security: {...}
KeyTypeDefaultPurpose
descriptionstringFree text, shown in the editor.
domainslistSIP domains this tenant's phones register with. Must be unique across tenants.
didsobjectNumbers this tenant owns on the shared trunks.
callerIdobjectOverrides the trunk's caller ID on outgoing calls.
timezonestringglobal timezoneIANA timezone for this tenant's time conditions.
interTenantlistTenants this one may dial. Only effective with global allowInterTenant.
mohobjectMusic-on-hold classes, layered over the global ones.
featureCodesobjectValidated and never dispatched on. See below.
dataDirpath<global dataDir>/<name>This tenant's own writable root. Written back exactly as typed.
recordingobjectglobalOverrides the global block field by field. Only enabled and mode are honoured.
storageobjectglobalMerged and then ignored — the global bucket is used.
voicemailobjectglobalOverrides the global block field by field.
smtpobjectglobalOverrides the global relay field by field.
securityobjectgloballimits is enforced; acl and requireSrtp are not.
livekitobjectglobalMobile apps. Replaces the global block outright rather than merging.

metadata.name is the tenant identifier. It is the one global kind that namespaces others, so it carries no metadata.tenant of its own.

dids

KeyTypeNotes
numberslistExact claims. Checked before patterns.
patternslistGo regular expressions. Not anchored implicitly.

A number claimed by two tenants is a validation error.

callerId

KeyTypeNotes
numberstringPresented on outgoing calls, ahead of the trunk's.
namestringDisplay name.

moh

spec:
moh:
default: ../sounds/moh/acme.wav
classes:
jazz: ../sounds/moh/acme-jazz.wav

Layered over the global map key by key: a class defined in both resolves to the tenant's file.

featureCodes

spec:
featureCodes:
voicemail: "*97"
dndOn: "*78"
dndOff: "*79"
forwardSet: "*72"
forwardClear: "*73"
queueLogin: "*11"
queueLogout: "*12"
recordToggle: "*1"
Nothing dispatches on this block

All eight fields are validated — a code that collides with an account extension or a dialplan extension is refused — and none of them make a number work. Dialling *78 reaches a feature only because an Extension document is named *78.

The one value read at run time is voicemail, and only to fill {{.FeatureCode}} in a notification email. See Feature codes.

Overrides of the global blocks

recording, storage, voicemail, smtp and security take the same shape as the System blocks and override them field by field — a key the tenant does not mention keeps the global value.

What actually takes effect per tenant:

BlockHonoured
recordingenabled and mode only.
storagenothing — the global bucket is used.
voicemailyes.
smtpyes.
securitylimits only. acl and requireSrtp are not enforced anywhere.
livekityes — but it replaces the global block instead of merging into it, so a tenant override must be complete.

Account

A SIP endpoint. metadata.name is the SIP username.

configVersion: lyno/v1
kind: Account
metadata:
name: alice
tenant: acme
spec:
password: "..."
extension: "100"
displayName: Alice
KeyTypeRequiredNotes
passwordstringyesDigest password, stored in plain text.
extensionstringyesExtension this account belongs to. Several accounts may share one, and then ring in parallel.
displayNamestringnoCaller name on internal calls.
recordboolnoOverrides the recording mode for calls this account answers or places. Unset follows the tenant.

Extension

A dialplan. metadata.name is the number, which is why *78 is an ordinary extension and feature codes need no special case.

configVersion: lyno/v1
kind: Extension
metadata:
name: "200"
tenant: acme
spec:
description: Support
steps:
- id: ring
type: dial
timeout: 20s
target: {kind: endpoint, endpoint: "100"}
KeyTypeRequiredNotes
descriptionstringnoFree text, shown in the editor.
stepslistyesOrdered. Every step type and field is in the Extension reference.

Steps stay a list inside the document: they are a sequence by nature, and one plan is one thing to read.

RingGroup

configVersion: lyno/v1
kind: RingGroup
metadata:
name: support
tenant: acme
spec:
strategy: simultaneous
targets: ["100", "101", "external:0612345678"]
timeout: 30s
KeyTypeRequiredNotes
strategystringyessimultaneous or sequential.
targetslistyesAt least one target reference.
timeoutdurationnoTotal ring time for simultaneous; per-target time for sequential. Defaults to 30s.

Queue

configVersion: lyno/v1
kind: Queue
metadata:
name: support
tenant: acme
spec:
strategy: fewestrecent
agents: ["100", "101"]
ringTimeout: 20s
maxWait: 5m
moh: default
announce: ../sounds/still-holding.wav
announceEvery: 45s
joinEmpty: false
KeyTypeRequiredDefaultNotes
strategystringyesringall, linear, roundrobin or fewestrecent.
agentslistyes*Account extensions of this tenant.
ringTimeoutdurationno20sOne attempt at one turn's agents.
maxWaitdurationnoforeverThen the caller continues to the next step.
mohstringnoringbackClass name, resolved in the tenant then globally.
announcepathnoPlayed periodically while waiting.
announceEverydurationnoRequires announce.
joinEmptyboolnofalseQueue a caller even when no agent is staffed.
dynamicboolnofalseAccepted, and an agent can never log in.

* Either agents or dynamic: true. See Call queues.

IVRMenu

configVersion: lyno/v1
kind: IVRMenu
metadata:
name: main
tenant: acme
spec:
prompt: ../sounds/menu-main.wav
timeout: 5s
repeat: 3
options:
"1": {type: extension, target: "200"}
"0": {type: hangup}
invalidFile: ../sounds/menu-invalid.wav
timeoutFile: ../sounds/menu-timeout.wav
onTimeout: {type: extension, target: "200"}
directDial: true
KeyTypeRequiredDefaultNotes
descriptionstringnoFree text, shown in the editor.
promptpathyesPlayed first, interruptible by the first key.
optionsmapyes*Key → destination. Keys may be several digits.
timeoutdurationno5sWait for a digit before repeating.
repeatintno3Attempts before giving up.
invalidFilepathnoPlayed after a wrong key.
timeoutFilepathnoPlayed after silence.
onInvaliddestinationnofall through
onTimeoutdestinationnofall through
directDialboolnofalseLet a caller dial an extension not on the menu.

* Either options or directDial: true.

Options stay a map: a keypad has no order beyond which key was pressed.

Mailbox

metadata.name is the extension whose messages it holds, and it must be an account extension of the tenant.

configVersion: lyno/v1
kind: Mailbox
metadata:
name: "100"
tenant: acme
spec:
displayName: Alice
pin: "4821"
email: alice@example.com
attach: true
deleteAfterEmail: false
greeting: ../sounds/alice-greeting.wav
KeyTypeRequiredDefaultNotes
displayNamestringnoUsed in the notification email.
pinstringyes4 to 8 digits, stored in the clear, compared in constant time.
emailstringnoNotification address. Needs an SMTP relay.
attachboolnotrueSend the recording with the notification.
deleteAfterEmailboolnofalse
greetingpathnoReplaces the shipped prompts for this box.
maxMessagedurationnotenant voicemail.maxMessage
retentiondurationnotenant voicemail.retention

InboundRoute

Where one of this tenant's DIDs is delivered.

configVersion: lyno/v1
kind: InboundRoute
metadata:
name: main-line
tenant: acme
spec:
priority: 10
did: "+31201234567"
destination: {type: extension, target: "200"}
KeyTypeRequiredNotes
priorityintnoAscending; ties break on metadata.name. A catch-all belongs last.
didstringyesThe number as it arrives, or * for a catch-all inside the tenant.
destination.typestringyesUse extension or ring_group.
destination.targetstringyesExtension number, or ring group name. Must exist.
recordboolnoOverrides the recording mode for calls on this number.

Validation accepts all seven destination types, but only extension and ring_group are delivered — see Inbound routing.

OutboundRoute

The one kind that may omit metadata.tenant. With a tenant it is tried first; without one it is the global fallback.

configVersion: lyno/v1
kind: OutboundRoute
metadata:
name: national
tenant: acme
spec:
priority: 20
pattern: '^0[1-9]\d{8}$'
strip: 1
prepend: "+31"
trunk: carrier
KeyTypeRequiredNotes
priorityintnoAscending; first match wins, so this is load-bearing.
patternstringyesGo regular expression, matched against the dialled number.
stripintnoDigits removed from the front, applied before prepend.
prependstringnoText put in front of what remains.
trunkstringyesMust name a Trunk.

See Outbound routing.

Webhook

An HTTPS endpoint that receives this tenant's events. Delivery defaults live in System.webhooks.

configVersion: lyno/v1
kind: Webhook
metadata:
name: crm
tenant: acme
spec:
url: https://crm.example.com/hooks/lyno
secretFile: /etc/lyno/secrets/crm
events: [call.answered, call.ended]
timeout: 10s
KeyTypeRequiredDefaultNotes
urlstringyesMust be https unless insecureHttp is set.
secretstringnoSigning secret. The least good option — see below.
secretFilepathnoRead at startup and trimmed.
eventslistnoallSubset of the event names below.
timeoutdurationnoglobal webhooks.timeoutBounds one delivery attempt.
insecureHttpboolnofalsePermits a plain http URL.

The signing secret is resolved most-secure-first:

  1. LYNO_WEBHOOK_SECRET_<TENANT>_<NAME>, uppercased, with - and . mapped to _;
  2. secretFile;
  3. the inline secret.

Because that mapping can collide — a-b and a_b share one variable — two endpoints of one tenant whose mapped names meet are refused. An endpoint may sign nothing, so an empty result is legal.

insecureHttp exists for an endpoint on loopback or a private segment and is a footgun anywhere else: a signing secret sent over plain HTTP is a secret on the wire.

Event names

A closed set. A misspelt event is a startup error rather than a subscription that quietly never fires.

EventFires when
call.ringingA call starts alerting.
call.answeredA call is answered and bridged.
call.endedA call finishes, however it ended.
mobile.ringingA follow-me or external leg starts alerting.
mobile.endedThat leg finishes.
voicemail.receivedA message is stored.
registration.upAn endpoint registers.
registration.downIts registration lapses or is dropped.

Target references

Wherever a target is accepted — ring group targets, follow-me stage targets, a dial step's endpoint — these three forms are valid:

FormMeaning
"101"An account extension of this tenant.
external:0612345678A number placed through the outbound routes.
tenant:globex/900Another tenant's extension, when inter-tenant calling is allowed on both switches.

Validation

  • RingGroup: invalid strategy; no targets; a target that is not a valid reference.
  • InboundRoute: an invalid destination type; an unknown extension, ring group, queue, menu or mailbox; an external destination with no number.
  • OutboundRoute: a pattern that does not compile; an unknown trunk.
  • Queue: an invalid strategy; no agents and dynamic off; an agent that is not an account extension of the tenant; negative durations; a music class no level defines; an unreadable announce; announceEvery with no announce.
  • IVRMenu: no prompt; an unreadable prompt, invalidFile or timeoutFile; no options and no directDial; an empty option key; a key containing something that is not a telephone key; an invalid destination; a negative repeat or timeout.
  • Mailbox: a name that is not an account extension of the tenant; a missing PIN, or one that is not 4 to 8 digits; a non-numeric PIN; an unreadable greeting; negative durations; an invalid email address; an email address with no SMTP relay configured globally or on the tenant.
  • Webhook: a URL that is not https without insecureHttp; an unknown event name; an unreadable secretFile; two endpoints of one tenant whose secret environment variables collide.
  • featureCodes: a code colliding with an account extension or a dialplan extension; a voicemail code on a tenant with no mailboxes.
  • The overriding blocks are validated with the same rules as the global ones.
  • Target references: an extension that is not an account extension of the tenant; an empty external: target; a tenant: reference without allowInterTenant, without the tenant in interTenant, naming an unknown tenant, or naming an extension that tenant has no account for.
  • Across tenants: duplicate domains and duplicate DIDs.
  • Any tenant-scoped document whose metadata.tenant names a tenant that does not exist.