Lyno · an open-source PBX by telqo
tenants/acme/extensions/200.yamlis a complete phone system.
Lyno is an open-source, multi-tenant PBX in one Go binary. Extensions, trunks and opening hours are YAML files — versioned in git, reviewed like code.
one binary · 19 MB image · no database · no web panel
Phone systems got complicated. Lyno disagrees.
Somewhere between the rack in the closet and the cloud panel with four hundred checkboxes, phone systems stopped being something one person could hold in their head. Lyno is the counter-offer.
It’s files, not a control panel
A tenant is a folder. A dialplan is a file. Opening hours change in a pull request, get a review, and bad ideas get a git revert — instead of clicking through forms at 2 a.m.
Tenants stay in their lane
Every customer brings its own domains, numbers and extension plan. Passwords live in the tenant’s own realm, so acme’s credentials never open globex’s door.
Boring to operate, on purpose
Configuration is read once at startup and validated as a whole — every mistake reported in one pass, before the PBX takes a single call. Restart to apply. That’s the ops manual.
This is the whole thing. Really.
One shared file for listeners, trunks and global routing. One folder per tenant, self-contained enough to copy between machines. One file per extension. There is no step four.
Anything that can be global or per-tenant resolves the same way: the tenant’s value if set, otherwise the global one. Validation reads the whole tree and reports every problem in one pass — the editor literally cannot save a config the PBX would refuse.
sip:
bind_host: 0.0.0.0
bind_port: 5060
trunks:
- name: carrier
host: sip.provider.example
auth:
username: "12345678"
password: "${CARRIER_PASSWORD}"
registration:
enabled: true
outbound_routes:
- name: national
pattern: '^0[1-9]\d{8}$'
strip: 1
prepend: "+31"
trunk: carrier
default_trunk: carrier
tenants_dir: tenants
timezone: Europe/Amsterdam
Everything a phone system needs. Nothing it doesn’t.
Multi-tenant for real
The SIP domain picks the tenant for phones, the DID picks it for inbound calls, and the digest realm keeps every password at home. Extension 100 exists five times without a fight.
Trunks that find themselves
Provider gateways are discovered from DNS and refreshed every five minutes. A carrier that moves its gateways on a Friday does not take your inbound calls with it.
Dialplans read top to bottom
play, dial, moh, hangup — in order. A dial nobody answers falls through to the next step, which is why “greet, ring everyone, say we’re closed” is one short file.
Follow-me that follows
The desk first, then the mobile, then the colleague. First answer wins and everyone else stops ringing — including a loser that answered a heartbeat too late.
Opening hours built in
Days, hour ranges that wrap midnight, holiday date ranges, a timezone per tenant. Any step can carry a when: — and not: true inverts it when you feel brave.
A dashboard in your terminal
Calls, registrations, trunk state and logs, filterable by tenant — the numbers on screen always describe what you’re looking at. -headless when it’s systemd’s turn to watch.
An editor that can’t break prod
lyno -configure walks the whole configuration, validates before it writes, and refuses to delete anything still referenced. It cannot save a file the PBX would refuse to load.
One 19 MB binary
A distroless image with no shell and no database to babysit, or a single Go binary on a host. If you can run a static binary, you can run a phone company.
Built for people who live in a terminal
Start lyno and you’re looking at your PBX: calls, registrations, trunks and logs in one view. Filters are global — narrow to a tenant and every number on screen narrows with you. And finished calls stay on screen, because “what just happened?” is the whole reason you opened the dashboard.
┌─ Lyno PBX by Telqo ────────────────── 0.0.0.0:5060 · 4/5 online · 1/1 trunks up · uptime 6h12m ─┐ │ Overview Calls 2 Endpoints 5 Trunks 1 Logs tenant acme │ ├───────────────────────────────────────────────────────────────────────────────────────────────────┤ │ active calls endpoints trunks calls since start │ │ 2 4/5 1/1 148 │ │ in progress registered reachable 131 answered · 15 missed · 2 live │ │ │ │ Tenants │ │ TENANT DOMAIN ONLINE CALLS DIDS │ │ acme acme.pbx.example.com 3/3 2 2 active │ │ globex globex.pbx.example.com 1/2 0 1 idle │ │ │ │ Recent calls │ │ TIME TENANT FROM TO STATE DURATION │ │ 14:02:11 acme +31612345678 200 answered 00:04:31 │ │ 13:58:02 acme 100 101 answered 00:00:47 │ │ 13:41:19 globex +31201112233 900 missed — │ └───────────────────────────────────────────────────────────────────────────────────────────────────┘ tab switch · t tenant · / filter · ? help · q quit
Ten minutes and a softphone
That’s what the quick start needs — no trunk, no carrier contract, no cloud account. Point -config at a path that doesn’t exist and Lyno bootstraps a fresh system for you.