betterComms
An org-wide communications hub that gives engineers control over what they hear and when.
Admins publish structured messages to named channels. Engineers subscribe to the channels and tags they care about and pull a personalised RSS feed. High-severity messages (incidents, warnings) are also pushed via Teams webhook, email, ntfy.sh, and Viva Engage.
The problem
Engineering teams suffer from communication overload: too many Slack channels, too many email lists, no opt-in point, and no relevance filtering. Important incident notices get lost in the noise; routine announcements interrupt focus work.
betterComms separates signal from noise by making subscriptions explicit and feed-reader-native.
How it works
- An admin creates a channel (e.g.
platform-incidents, releases-payments). Subscribers can request new channels; admins approve or reject requests.
- Admins or CI/CD pipelines publish messages to a channel via REST API or the web UI. Each message has a title, Markdown body, severity (
info, planned, release_note, warning, incident), and optional tags.
- Engineers subscribe to the channels they care about, optionally filtering by tags.
- Each engineer gets a private RSS/Atom feed URL they add to their feed reader. The feed shows only messages from their subscribed channels and tags.
- For
warning and incident severity messages, the service also pushes to configured channels: Teams webhook, SMTP email, ntfy.sh, and/or Viva Engage.
See USER-GUIDE.md for step-by-step instructions for subscribers and admins.
Stack
| Layer |
Technology |
| Runtime |
Node.js 22, TypeScript |
| HTTP server |
Fastify 4.x with fastify-type-provider-zod |
| ORM |
Prisma 5.x |
| Database |
PostgreSQL 16 |
| Frontend |
React 18 + Vite 5 (served by Fastify in production) |
| Testing |
Vitest (68 integration tests against real Postgres) |
| Container |
Docker (multi-stage Alpine image) |
| Local dev |
docker-compose |
| Auth |
Session cookie + optional OIDC SSO |
Project layout
service-betterComms/
├── src/
│ ├── index.ts # entry point
│ ├── app.ts # Fastify app + plugin registration
│ ├── routes/ # route handlers by domain
│ │ ├── auth.ts # login, logout, register, /auth/me, OIDC
│ │ ├── channels.ts # channel CRUD + approval flow
│ │ ├── messages.ts # publish, list, edit, delete messages
│ │ ├── subscriptions.ts # subscribe / unsubscribe / list
│ │ ├── feeds.ts # RSS/Atom feed generation
│ │ └── me.ts # feed token, personalised feed JSON
│ ├── lib/ # prisma client, OIDC client
│ └── workers/ # push delivery polling worker
├── packages/
│ └── web/ # React + Vite SPA
│ └── src/pages/
│ ├── Login.tsx
│ ├── ChannelList.tsx
│ ├── ChannelDetail.tsx # subscriber view
│ ├── AdminChannelDetail.tsx # admin CRUD view
│ └── MyFeed.tsx
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── scripts/
│ └── seed.ts # dev seed with Harness CI/CD sample data
├── test/ # Vitest integration tests
├── Dockerfile # multi-stage Alpine build
├── entrypoint.sh # runs prisma migrate deploy then starts server
├── docker-compose.yml
├── PLAN.md # delivery slices and task checklist
├── ARCHITECTURE.md # design decisions and rationale
└── USER-GUIDE.md # end-user and admin documentation
Running with Docker (recommended)
# Build image and start Postgres + app
docker compose up -d --build
# App is available at http://localhost:3001
# Database migrations run automatically on container start
To seed the database with sample data (Harness CI/CD scenario):
npm run db:seed
Running in development
# Start Postgres (port 5434) and test-Postgres (port 5433)
docker compose up -d db test-db
# Install dependencies
npm install
# Apply migrations to dev database
npm run db:migrate
# Start API server with hot reload (port 3000)
npm run dev
# In a separate terminal: start Vite SPA dev server (port 5173, proxies API to :3000)
cd packages/web && npm run dev
Running tests
# Requires test-db to be running (docker compose up -d test-db)
npm test
Environment variables
Required
| Variable |
Description |
DATABASE_URL |
Postgres connection string — postgresql://user:pass@host:5432/db |
SESSION_SECRET |
Secret for session signing — minimum 32 characters, random |
Auth
| Variable |
Default |
Description |
ALLOW_LOCAL_REGISTRATION |
false |
Set true to enable POST /auth/register (dev/early setup) |
COOKIE_SECURE |
false |
Set true when serving over HTTPS — required for Secure cookie flag |
API_KEY |
— |
Plain-text publisher API key for CI/CD pipelines (stored unhashed at runtime) |
OIDC_ISSUER_URL |
— |
OIDC provider discovery URL (e.g. https://accounts.google.com) |
OIDC_CLIENT_ID |
— |
OIDC client ID |
OIDC_CLIENT_SECRET |
— |
OIDC client secret |
OIDC_REDIRECT_URI |
— |
Callback URL registered with the OIDC provider (e.g. https://bettercomms.example.com/auth/oidc/callback) |
Push delivery
Push delivery is disabled by default. Set PUSH_DELIVERY_ENABLED=true to activate. Each channel is independently optional.
| Variable |
Description |
PUSH_DELIVERY_ENABLED |
Set true to start the push delivery worker |
PUSH_POLL_INTERVAL_MS |
Polling interval in ms (default 30000) |
TEAMS_WEBHOOK_URL |
Microsoft Teams incoming webhook URL |
SMTP_HOST |
SMTP server hostname |
SMTP_PORT |
SMTP port (default 587) |
SMTP_USER |
SMTP username (omit for unauthenticated relay) |
SMTP_PASS |
SMTP password |
SMTP_FROM |
From address for outgoing email |
SMTP_TO |
Destination address(es) for push email |
NTFY_TOPIC_URL |
ntfy.sh topic URL (e.g. https://ntfy.sh/your-org-alerts) |
VIVA_ENGAGE_TOKEN |
Viva Engage (Yammer) bearer token |
VIVA_ENGAGE_GROUP_ID |
Viva Engage group ID to post into |
Key API endpoints
Auth
| Method |
Path |
Auth |
Description |
POST |
/auth/register |
— |
Register (requires ALLOW_LOCAL_REGISTRATION=true) |
POST |
/auth/login |
— |
Login with email + password |
POST |
/auth/logout |
Session |
Log out |
GET |
/auth/me |
Session |
Return current user |
GET |
/auth/oidc/login |
— |
Redirect to OIDC provider |
GET |
/auth/oidc/callback |
— |
OIDC callback — creates/updates user and sets session |
Channels
| Method |
Path |
Auth |
Description |
GET |
/channels |
Session |
List channels (admins see all; subscribers see active only) |
GET |
/channels/:slug |
Session |
Get a single channel |
POST |
/channels |
Session |
Create channel (admin → active; subscriber → pending approval) |
GET |
/channels/pending |
Admin |
List channels awaiting approval |
PATCH |
/channels/:slug/status |
Admin |
Approve or reject a pending channel |
Messages
| Method |
Path |
Auth |
Description |
GET |
/channels/:slug/messages |
Session |
List messages for a channel |
POST |
/channels/:slug/messages |
Publisher |
Publish a new message |
PATCH |
/channels/:slug/messages/:id |
Admin |
Edit a message |
DELETE |
/channels/:slug/messages/:id |
Admin |
Delete a message |
Subscriptions
| Method |
Path |
Auth |
Description |
GET |
/subscriptions |
Session |
List my subscriptions |
POST |
/subscriptions |
Session |
Subscribe to a channel (with optional tag filters) |
DELETE |
/subscriptions/:channelSlug |
Session |
Unsubscribe |
My feed
| Method |
Path |
Auth |
Description |
GET |
/me/feed |
Session |
Personalised feed as JSON |
GET |
/me/feed-token |
Session |
Get my RSS feed token |
POST |
/me/feed-token/regenerate |
Session |
Rotate feed token (old URL stops working) |
Feeds (public / token-authenticated)
| Method |
Path |
Auth |
Description |
GET |
/feeds/channel/:slug/rss.xml |
None |
Public RSS feed for a channel |
GET |
/feeds/user/:feedToken/rss.xml |
Token in URL |
Personalised RSS feed |
Feed token URLs must be kept private — anyone with the URL can read the feed. Rotate via POST /me/feed-token/regenerate if compromised.
Publishing via API key (CI/CD)
curl -X POST https://bettercomms.example.com/channels/my-channel/messages \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-api-key" \
-d '{
"title": "Deployment v2.14.0 complete",
"body": "## What changed\n\n- Feature X shipped\n- Bug Y fixed",
"severity": "release_note",
"tags": ["payments", "v2.14"]
}'
Valid severity values: info, planned, release_note, warning, incident
Delivery slices
| Slice |
Status |
Summary |
| 1 |
Complete |
Core scaffold: channels, messages, RSS feed |
| 2 |
Complete |
Web UI, subscriptions, personalised feed |
| 3 |
Complete |
Push delivery: Teams, email, ntfy.sh, Viva Engage |
| 4 |
Complete |
Channel approval flow, OIDC SSO, Backstage plugin |
| 5 |
In progress |
Single deployable image (Docker build done; scaling TBD) |
| 6 |
Planned |
Security testing & QE hardening |
| 7 |
Planned |
Settings page (themes, fonts, delivery preferences) |
See PLAN.md for the full task checklist and ARCHITECTURE.md for design decisions.