This guide walks through apps/chat, a Slack/Discord-style realtime chat app that shows how a normal production app fits into Zomg. The app has:
  • A Bun TypeScript API with REST setup/history endpoints and a websocket realtime API.
  • PostgreSQL persistence for users, sessions, workspaces, channels, memberships, messages, mentions, reactions, pins, read receipts, and file metadata.
  • Files stored on a Zomg data volume mounted at /data/files.
  • A compact React + shadcn frontend that exercises the API.
  • Fast integration tests that boot a temporary local PostgreSQL server and drive the public HTTP/WebSocket API in parallel.

App layout

apps/chat/
  src/server/        # Bun HTTP + websocket server
  src/client/        # React + shadcn demo client
  tests/             # Integration tests against the live server
  box.compose.yaml  # Optional stack example
scripts/verify/chat_app.py
Run the local verification target before deploying:
just verify-chat-app
just chat-check
just verify-chat-app starts disposable PostgreSQL with initdb, creates an isolated schema per test, starts the real Bun server on random ports, then runs bun test --jobs=8. The tests do not call private functions; they register users, create workspaces/channels, open websockets, send messages, upload files, search, react, pin, and check permissions through the public API.

Runtime architecture

Use three pieces in Zomg:
PieceZomg primitiveWhy
chat-dbzomg pg Postgres serviceDurable relational state and fast snapshots/boxes
chat-fileszomg data volumeUploaded files survive app box deletion
chat-appzomg service on a boxNamed process, logs, restart, publish, preview/promote
Inside the app box, Postgres is reached by Zomg DNS:
postgres://postgres:postgres@chat-db:5432/postgres
The file volume is mounted at /data, so the app stores file bytes under /data/files. It’s kept separate from the app root filesystem on purpose: deleting or recreating chat-app does not delete uploads.

Create the backing services

Select a target first:
zomg config set profile dev
zomg health
Create Postgres and the file volume:
zomg pg new chat-db
zomg data create chat-files
Create the app box with the file volume already mounted:
zomg create chat-app --cpu 2 --memory 3G --data chat-files=/data

Copy and build the app

Package only the app source:
tar -czf /tmp/zomg-chat.tgz \
  --exclude='node_modules' \
  --exclude='dist' \
  -C . apps/chat
Copy it into the app box and unpack it:
zomg exec chat-app -- bash -lc 'mkdir -p /work/zomg'
zomg cp /tmp/zomg-chat.tgz chat-app:/tmp/zomg-chat.tgz
zomg exec chat-app -- bash -lc 'cd /work/zomg && tar -xzf /tmp/zomg-chat.tgz'
Install Bun if the base does not have it, then install and build:
zomg exec chat-app -- bash -lc 'command -v bun >/dev/null || curl -fsSL https://bun.sh/install | bash'
zomg exec chat-app -- bash -lc '
  export PATH="$HOME/.bun/bin:$PATH"
  cd /work/zomg/apps/chat
  bun install --frozen-lockfile
  bun run build
'

Start and publish the app service

Run the web/API process as a named service:
zomg service start chat-app web \
  --port 8080 \
  --publish chat \
  -- bash -lc '
    export PATH="$HOME/.bun/bin:$PATH"
    cd /work/zomg/apps/chat
    DATABASE_URL=postgres://postgres:postgres@chat-db:5432/postgres \
    FILE_STORAGE_DIR=/data/files \
    CHAT_HOST=0.0.0.0 \
    CHAT_PORT=8080 \
    bun run start
  '
Inspect it:
zomg service status chat-app web
zomg service logs chat-app web --lines 100
The service owns the public URL lifecycle. Use zomg service publish, unpublish, restart, and stop; avoid zomg exec --bg for the app process.

Blue/green deploy

For the next build, copy new source into the same box, install/build, then deploy a preview on a different port:
zomg service deploy chat-app web \
  --preview chat-preview \
  --port 8081 \
  -- bash -lc '
    export PATH="$HOME/.bun/bin:$PATH"
    cd /work/zomg/apps/chat
    DATABASE_URL=postgres://postgres:postgres@chat-db:5432/postgres \
    FILE_STORAGE_DIR=/data/files \
    CHAT_HOST=0.0.0.0 \
    CHAT_PORT=8081 \
    bun run start
  '
Check the preview URL from zomg service status chat-app web. When it is good:
zomg service promote chat-app web
Promotion repoints the active host to the preview port, unpublishes the preview host, and kills the old active process.

Snapshots and backups

Before risky migrations, snapshot both durable stores:
zomg pg snapshot chat-db pre-chat-migration
zomg data snapshot create chat-files pre-chat-migration -m "before chat migration"
For regular off-box file backups:
zomg data backup create chat-files -m "$(date -u +chat-files-%Y%m%d)"
Postgres and file uploads are deliberately separate. zomg pg snapshot captures database state; zomg data snapshot captures uploaded file bytes.

API surface

The HTTP API handles setup, history, search, and file transfer:
EndpointPurpose
POST /api/auth/registerCreate a user and bearer token
POST /api/workspacesCreate a workspace
POST /api/workspaces/:id/membersAdd a workspace member
POST /api/workspaces/:id/channelsCreate public/private/DM channels
GET /api/channels/:id/messagesRead channel history
POST /api/channels/:id/messagesPersist a message
PATCH /api/messages/:id / DELETE /api/messages/:idEdit and soft-delete messages
POST /api/messages/:id/reactionsAdd emoji reactions
POST /api/messages/:id/pinPin a message
POST /api/channels/:id/readMark read receipts
GET /api/search?workspaceId=...&q=...Search readable messages
POST /api/files / GET /api/files/:idUpload and download files
The websocket endpoint is /api/ws?token=<token>. Client messages include:
{ "type": "subscribe", "payload": { "channelId": "..." } }
{ "type": "message.create", "payload": { "channelId": "...", "body": "hello" } }
{ "type": "typing", "payload": { "channelId": "...", "active": true } }
{ "type": "presence.set", "payload": { "status": "online" } }
{ "type": "reaction.add", "payload": { "messageId": "...", "emoji": ":+1:" } }
{ "type": "read.mark", "payload": { "channelId": "...", "messageId": "..." } }
Server events include connection.ready, channel.subscribed, message.created, message.updated, message.deleted, typing.updated, presence.updated, reaction.added, reaction.removed, message.pinned, message.unpinned, and read.updated.

Optional compose file

apps/chat/box.compose.yaml shows how to wire the app to a Postgres addon:
zomg compose up -f apps/chat/box.compose.yaml
Use the explicit zomg data create and zomg data attach commands for the file volume, since uploaded files are app data that should outlive any one service deployment.