Under the hood
A plain-language tour of the stack, the protocols, and the cryptography that holds T9phone together. If you don’t trust a system you can’t inspect, this is where to start.
Frontend
Next.js 14(App Router) on Node, served behind Cloudflare.React,Tailwind CSS,Radix UIprimitives,Framer Motionfor motion.- Strict Content-Security-Policy with per-request nonce. No third-party trackers or analytics scripts.
- Cloudflare Turnstile on the registration form (no captcha on token-gated invite flows).
Backend
Express(Node.js) for the portal API.PostgreSQLfor users, groups, invites, audit log, and SIP credentials.- HTTP-only,
Secure,SameSite=Laxsession cookies; CSRF token on state-changing requests. - Cloudflare may set a
__cf_bmbot-management cookie on edge-challenged requests. It is HTTP-only, ~30 minutes, used only to distinguish humans from bots — not for tracking or advertising. - Rate limiting on registration, login, password reset, and invite redemption.
- Transactional email via
Resend(verification, password reset, invitations). - Services run under
systemd(portal-backend,portal-frontend,portal-provisioning) with journald logs.
Voice
FreeSWITCHon a dedicated VPS handles SIP registration and call routing.- SIP user directory served from the portal (
/api/freeswitch/directory) over mutually-authenticated HTTPS — there are no static.xmluser files on the SIP box. - Dialplan served the same way (
/api/freeswitch/dialplan) — the closed-group rule lives in the portal, not the switch. coturnprovides ICE/STUN/TURN so calls connect through NAT and carrier-grade firewalls.bypass_mediamode: once the call is up, RTP flows endpoint-to-endpoint through TURN — the SIP server never sees the audio.- No PSTN trunking, no SIP federation. Calls cannot enter or leave the platform.
Closed-group routing
When a call arrives the dialplan looks up both legs in PostgreSQL. The call is bridged only if caller and callee share at least one group, and an active group is selected with the optional *N*<code> prefix. If no shared group exists the call is rejected at the switch — the destination is never rung.
Cryptography
- Account passwords:
bcryptat cost 12. Hashed, salted, not recoverable. Compares are constant-time and a dummy hash is verified for unknown users to prevent timing-based account enumeration. - SIP credentials:
AES-256-GCMwith a rotated key set. The plaintext only ever lives in memory long enough to answer a FreeSWITCH directory lookup. - Email verification & password reset tokens: 32 bytes of CSPRNG entropy; only their SHA-256 hash is stored. Expire on use and after a short TTL.
- Invite tokens: single- or multi-use bearer secrets, hashed at rest, redemption gated on the invitee’s email matching the invite when enforced.
Transport
- Web (
t9phone.com): TLS 1.2+, Cloudflare Full (strict) to the origin which presents a Cloudflare Origin cert. HSTS enabled. - SIP signalling (
sip.t9phone.com:5061): SIP over TLS using a Let’s Encrypt certificate (full chain delivered to clients). Plain UDP SIP is not exposed. - Media: ICE/TURN over UDP, with TLS-protected credential exchange. Acrobits clients run with ZRTP enabled where the peer supports it.
What we keep, and what we don’t
- Kept: account row (email, nickname, bcrypt hash), SIP credential row (AES-GCM ciphertext), group membership, invite state, group audit events (joins, leaves, bans, role changes).
- Not kept: call detail records, recordings, transcripts, call durations, voicemail, contact lists, message history, presence status, IP geolocation logs.
Self-verification
Everything above can be checked from your own device. Inspect the TLS certificates with openssl s_client, observe traffic flows with Wireshark, or read the deployed JavaScript in your browser’s devtools — there is no obfuscation. T9phone is built on open cryptography because secrecy of design is not security.