Skip to main content

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

Why Lyno

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.

Configuration

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.

configs/lyno.yaml
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
What’s in the box

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.

Operations

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.

./bin/lyno -config configs/lyno.yaml
┌─ 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.