Runtime architecture

ProBeya is a TypeScript modular monolith with a separate realtime service:

Internet
   |
   v
Nginx (TLS and hostname routing)
   |-----------------------------|
   v                             v
Next.js 16 `web`              Node.js `ws`
port 8000                    port 8003
- React 19 UI                - authenticated upgrades
- Auth.js                    - board rooms/presence
- tRPC and REST              - Redis pub/sub fan-out
- item-event worker
   |
   +---------------+------------------+
   v               v                  v
PostgreSQL 16     Redis 7        S3-compatible storage
Drizzle + RLS     cache/pub-sub   private attachments

There is no separate application API process. Next.js hosts the tRPC routers and HTTP routes implemented in packages/api. The production Compose stack also builds the Docusaurus site and routes it through Nginx.

Repository boundaries

apps/
├── web/               Next.js UI, Auth.js, tRPC/HTTP runtime
├── ws/                Node.js WebSocket service
├── docusaurus-docs/   Generated product/operator site
└── mintlify-docs/     Developer documentation source

packages/
├── api/               Routers, permissions, workers, integrations
├── db/                Drizzle schema and database operations
├── shared/            Validators, constants, and shared types
├── ui/                Shared React components
└── config/            Shared TypeScript and lint configuration

docker/
├── docker-compose.yml       Local infrastructure/base model
├── docker-compose.prod.yml  Production overlay
└── Dockerfile.*             Web, WS, docs, and schema images

Request and tenant lifecycle

  1. Nginx routes an HTTPS request to Next.js.
  2. The request layer classifies the host and Auth.js verifies the session.
  3. An authenticated tenant procedure resolves the organization from trusted session and membership state. It never trusts a client-supplied organizationId.
  4. The procedure starts a PostgreSQL transaction and authorizes SET LOCAL app.current_org_id against canonical membership data.
  5. Router-level ACL checks restrict the requested workspace, board, item, and column operation.
  6. Drizzle executes queries with explicit organization predicates.
  7. Canonical FORCE ROW LEVEL SECURITY policies restrict probeya_app as a final database boundary.

Application processes must use the probeya_app runtime role. The probeya_admin role can bypass RLS and is reserved for the one-shot schema operation and controlled local administration.

The core hierarchy is:

organizations
  └── workspaces
       └── projects
            └── boards
                 ├── groups
                 │    └── items
                 │         └── values
                 ├── columns
                 └── views

Tenant-scoped tables carry organization_id. Board reads and exports apply board grants, workspace membership, item visibility, and hidden-column rules in addition to the organization boundary.

API layer

The web application consumes tRPC v11 routers directly from packages/api, so input validation and response types are shared end to end:

const board = await trpc.boards.getById.query({
  boardId: "clx_board_packaging",
});

REST endpoints exist for external integration surfaces. Both REST and tRPC must enter the same authenticated tenant and permission boundaries.

Durable item side effects

Item create, update, move, value-change, and delete operations use a transactional PostgreSQL outbox:

  1. The item change and a versioned outbox event commit in one transaction.
  2. The worker running in the production web process claims pending events with bounded locks.
  3. Internal automation/notification state is processed in tenant context.
  4. External webhook, integration, and email effects carry stable event or idempotency metadata and retry with backoff.
  5. Failed events remain observable instead of being silently discarded.

The production Compose overlay sets ITEM_EVENT_OUTBOX_WORKER_ENABLED=true. Local development leaves it disabled unless side-effect processing is under test.

Realtime

apps/ws is a Node.js 22 HTTP/WebSocket process based on the ws package. It rejects an upgrade unless a short-lived signed token is valid. Clients then use typed messages such as join_board and leave_board; server authorization checks the immutable token claims before room changes.

Redis pub/sub fans board updates across WebSocket instances. GET /health reports WebSocket/Redis readiness for the container health check.

Storage

The object store uses two different origins:

  • S3_ENDPOINT is reachable by the server and may use http://minio:9000.
  • S3_PUBLIC_URL is reachable by browsers and must use the public TLS hostname.

The bucket is private. Anonymous reads are limited to explicit avatar and logo prefixes; attachments are downloaded only through ACL-checked signed URLs. Production upload writes fail closed if ClamAV is unavailable.

The current search path queries PostgreSQL in the authenticated tenant context. Results are filtered by organization, workspace/board ACLs, item visibility, and hidden-column rules before return. The supported Compose stack does not include Meilisearch.

Deployment

The supported self-hosted model combines the base Compose file with its production overlay. It builds web, ws, and docusaurus, plus an explicit one-shot schema image. Database changes use the operator-controlled sequence:

stop writes -> verified backup -> ownership adoption
  -> db:migrate -> apply-rls -> check-rls -> up

See Self-Hosting for the executable runbook.