Skip to content

Commit dfbfe02

Browse files
Copilotalliscodenshki
authored andcommitted
feat(hub): add production runtime and locked setup
Run the built frontend, API, and workers in one image. Keep schema and schedule setup in a separately locked operational script. Co-authored-by: Ben Thomas <25218250+alliscode@users.noreply.github.com> Co-authored-by: Nishiki <1121087+nshki@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 889ee0d commit dfbfe02

37 files changed

Lines changed: 1106 additions & 243 deletions

File tree

‎.github/workflows/hub-image.yml‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Hub image
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
image:
14+
name: Hub image smoke test
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 20
17+
steps:
18+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
19+
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
20+
with:
21+
node-version: 24
22+
- run: node hub/script/check-image/index.ts

‎hub/.dockerignore‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.env*
2+
!.env.example
3+
.devcontainer
4+
.local
5+
.npm
6+
node_modules
7+
dist
8+
coverage
9+
storybook-static
10+
server/generated
11+
**/*.test.*
12+
**/stories.tsx
13+
.storybook

‎hub/.env.example‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Optional API port. The Vite development proxy expects the default.
22
PORT=3001
3+
HOST=127.0.0.1
4+
5+
# The image defaults to combined; development starts the API alone.
6+
RUNTIME_MODE=api
7+
SHUTDOWN_TIMEOUT_MS=30000
8+
# CLIENT_DIRECTORY=/absolute/path/to/dist/client
39

410
# API logging threshold: trace, debug, info, warn, error, fatal, or silent.
511
LOG_LEVEL=info

‎hub/Dockerfile‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# syntax=docker/dockerfile:1
2+
FROM node:24-bookworm-slim AS build
3+
WORKDIR /app
4+
RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \
5+
&& rm -rf /var/lib/apt/lists/*
6+
COPY package.json package-lock.json .npmrc ./
7+
COPY schema ./schema
8+
COPY prisma7.config.ts ./
9+
RUN npm ci --engine-strict
10+
COPY . .
11+
RUN npm run build:runtime
12+
13+
FROM node:24-bookworm-slim AS runtime
14+
WORKDIR /app
15+
RUN apt-get update && apt-get install -y --no-install-recommends openssl ca-certificates \
16+
&& rm -rf /var/lib/apt/lists/*
17+
ENV NODE_ENV=production HOST=0.0.0.0 PORT=3001 RUNTIME_MODE=combined \
18+
CLIENT_DIRECTORY=/app/dist/client SHUTDOWN_TIMEOUT_MS=30000
19+
COPY --from=build /app/package.json /app/package-lock.json /app/.npmrc ./
20+
COPY --from=build /app/schema ./schema
21+
COPY --from=build /app/prisma7.config.ts ./
22+
COPY --from=build /app/node_modules ./node_modules
23+
RUN npm prune --omit=dev --ignore-scripts && npm cache clean --force
24+
COPY --from=build /app/dist ./dist
25+
USER node
26+
EXPOSE 3001
27+
CMD ["node", "dist/server/index.js"]

‎hub/README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
Hub is being rebuilt on `build/hub/new` using TypeScript, Express, React, Vite, and Vitest. This branch lets contributors start the new app locally. It is not ready to replace the existing Hub.
44

5-
The API has one route: `GET /api/health`. The frontend displays the existing Replica logo and supports light and dark themes. Existing components, features, and pages are kept in Storybook, but they are not connected to the new API.
5+
The API has liveness and readiness routes. The frontend displays the existing Replica logo and supports light and dark themes. Existing components, features, and pages are kept in Storybook, but they are not connected to product APIs.
66

7-
Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The [background-job foundation](docs/background-jobs.md) provides PostgreSQL-backed BullMQ queues and independent on-demand and scheduled workers, without product jobs yet. The database has no product models. Sign-in, saved product data, live updates, and deployment are not implemented.
7+
Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The [background-job foundation](docs/background-jobs.md) provides PostgreSQL-backed BullMQ queues and independent on-demand and scheduled workers, without product jobs yet. The database has no product models. Sign-in, saved product data, and live updates are not implemented.
8+
9+
The [production runtime](docs/deployment.md) packages the frontend, API, and workers in one image, with a separate release setup script.
810

911
Use [Development](docs/development.md) to start the API and frontend. Review [Architecture](docs/architecture.md), [code architecture conventions](docs/backend-architecture.md), and [coding conventions](docs/coding-conventions.md) before adding behavior.
1012

‎hub/docs/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Hub is being rebuilt. The [Hub overview](../README.md) describes what the baseli
66
- [Code architecture conventions](backend-architecture.md): repositories, integrations, utilities, and domains.
77
- [Coding conventions](coding-conventions.md): frontend layers and interface rules.
88
- [Development](development.md): local API and frontend setup.
9+
- [Deployment](deployment.md): production image, runtime roles, setup, and health.
910
- [Database](database.md): Prisma schema conventions, migrations, and the shared client.
1011
- [Authentication proof](authentication.md): internal provider proof and one-use authorization attempts.
1112
- [Logging](logging.md): application events, automatic request records, and payload safety.

‎hub/docs/architecture.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ The team has chosen TypeScript, Express for the API, React and Vite for the fron
88

99
The API responds to a health check, and the frontend displays the logo with light and dark themes. Storybook keeps the existing components and pages available for reuse.
1010

11-
Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The database has a provider-neutral identity transaction foundation and internal GitHub authentication proof primitives. No public authentication flow is exposed. Deployment and product APIs have not been built. This app cannot yet find repositories, import Builds, host previews, save product data, or support reviews.
11+
Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The database has a provider-neutral identity transaction foundation and internal GitHub authentication proof primitives, but it does not issue sessions or admit users to Hub. No public authentication flow or product API is exposed. This app cannot yet find repositories, import Builds, host previews, save product data, or support reviews.
12+
13+
The [production runtime](deployment.md) serves the built frontend and API and runs both workers in one image. Deployment runs database and job setup separately before starting that image.
1214

1315
## Backend
1416

1517
API code lives under `server/`. As features are added, route handlers will read requests, call domains, and return responses. The [code conventions](backend-architecture.md) explain repositories, integrations, utilities, domains, and which layers may import others. Put these directories under `server/` when they are needed; do not add empty implementations.
1618

17-
`GET /api/health` returns `{ "status": "ok" }`. Authentication routes are not registered. Unknown paths return a JSON 404 response; invalid JSON is rejected before routing. `createApp()` builds the Express app without starting a server. `server/index.ts` starts it at a local address.
19+
`GET /api/health` returns `{ "status": "ok" }`; `GET /api/ready` reports operational readiness. Authentication routes are not registered. Unknown API paths return a JSON 404 response; invalid JSON is rejected before routing. `createApp()` builds the Express app without starting a server. `server/index.ts` selects a runtime role and defaults to the local API.
1820

1921
The [database guide](database.md) defines the multi-file Prisma schema and shared client in integrations. The health route remains a process check, not a database readiness check.
2022

‎hub/docs/backend-architecture.md‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
Organize backend functions into routes, repositories, integrations, utilities, and domains. Each layer has a clear job and rules for which other layers it may use.
44

5-
| Layer | Responsibility | Directory example |
6-
| --- | --- | --- |
7-
| Routes | Parse HTTP requests, call backend functions, and send responses. Directories follow URL paths and HTTP methods. | `routes/api/health/get/index.ts` |
8-
| Repositories | Functions that interact with the database. Each repository directory represents one RESTful resource. | `repositories/user/get-user/index.ts` |
9-
| Integrations | Functions that interact with an API or SDK. Each integration directory represents one API or SDK service. | `integrations/github/get-user/index.ts` |
10-
| Utilities | General functions reusable elsewhere in the codebase. | `utils/format-timestamp/index.ts` |
11-
| Domains | Business logic functions that may compose repositories, integrations, and utilities. | `domains/user/get-user-profile/index.ts` |
5+
| Layer | Responsibility | Directory example |
6+
| ------------ | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
7+
| Routes | Parse HTTP requests, call backend functions, and send responses. Directories follow URL paths and HTTP methods. | `routes/api/health/get/index.ts` |
8+
| Repositories | Functions that interact with the database. Each repository directory represents one RESTful resource. | `repositories/user/get-user/index.ts` |
9+
| Integrations | Functions that interact with an API or SDK. Each integration directory represents one API or SDK service. | `integrations/github/get-user/index.ts` |
10+
| Utilities | General functions reusable elsewhere in the codebase. | `utils/format-timestamp/index.ts` |
11+
| Domains | Business logic functions that may compose repositories, integrations, and utilities. | `domains/user/get-user-profile/index.ts` |
1212

1313
## Import boundaries
1414

@@ -37,6 +37,8 @@ Export route handlers by HTTP method, such as `get()`, and alias them to descrip
3737

3838
Add directories when their functions are needed. Do not add empty implementations just to fill out the structure.
3939

40+
Operational entry points belong under `server/scripts/`. They may compose backend functions and run deployment tools, but routes and domains must not import them. Setup runs separately from the API and workers.
41+
4042
## Database access
4143

4244
The [shared Prisma client](database.md#shared-client) lives in `server/integrations/prisma/index.ts`. Domains may pass it to repositories for database queries without letting repositories import integrations. Keep model definitions, naming, and migrations consistent with the [database conventions](database.md#schema-conventions).

‎hub/docs/background-jobs.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ Hub uses BullMQ's PostgreSQL backend for internal background work. The foundatio
66

77
Two stable queue names separate execution capacity:
88

9-
| Queue | Work | Command from `hub/` |
10-
| --- | --- | --- |
11-
| `on-demand` | Work requested by an application action | `npm run worker:on-demand` |
9+
| Queue | Work | Command from `hub/` |
10+
| ----------- | --------------------------------------------------- | -------------------------- |
11+
| `on-demand` | Work requested by an application action | `npm run worker:on-demand` |
1212
| `scheduled` | Recurring work and work spawned by recurring sweeps | `npm run worker:scheduled` |
1313

1414
Each command starts one Node.js process consuming only its assigned queue. The API starts neither worker. Multiple processes may consume the same queue when more capacity is needed. Separate queues do not isolate PostgreSQL, CPU, or external service quotas.
@@ -17,7 +17,7 @@ Each command starts one Node.js process consuming only its assigned queue. The A
1717

1818
`server/workers/configuration.ts` is the composition point for processors and schedules. Its empty registrations are intentional until product domains exist. Worker startup logs readiness after connecting; unknown job names fail permanently rather than disappearing.
1919

20-
Workers stop accepting work on `SIGINT` or `SIGTERM` and wait for active jobs. After 30 seconds, the command exits unsuccessfully rather than waiting forever. Interrupted work can be retried by BullMQ's stalled-job recovery. Deployment stop timeouts must allow this shutdown window. Long-running domain work needs its own bounded execution and interruption policy; a queue lock is not a business lock.
20+
Workers stop accepting work on `SIGINT` or `SIGTERM` and wait for active jobs. After `SHUTDOWN_TIMEOUT_MS` (30000 milliseconds by default), the command exits unsuccessfully rather than waiting forever. Interrupted work can be retried by BullMQ's stalled-job recovery. Deployment stop timeouts must allow this shutdown window. Long-running domain work needs its own bounded execution and interruption policy; a queue lock is not a business lock.
2121

2222
## Database and setup
2323

@@ -42,7 +42,7 @@ npm run start:worker:on-demand
4242
npm run start:worker:scheduled
4343
```
4444

45-
Run the last three commands as separate services, not sequential commands in one shell. These roles can use one application image in a future Compose deployment, with PostgreSQL shared and only the web service exposed. Production frontend hosting, an application image, service health checks, and release orchestration are not implemented by this foundation.
45+
Run the last three commands as separate services, not sequential commands in one shell. The [production image](deployment.md) instead defaults to a combined process that runs the API and both workers and coordinates their shutdown.
4646

4747
Each queue or worker owns a small query pool and BullMQ's additional notification connections. The query pool is limited to two connections; concurrency does not increase that limit. Budget database connections across all API, producer, and worker instances. Creation functions require an explicit URL so tests and other callers cannot accidentally inherit a previously imported database client.
4848

@@ -52,14 +52,14 @@ Keep stable names, a Zod payload schema, and retry options in a job definition.
5252

5353
Use the functions under `server/integrations/bullmq/`:
5454

55-
| Function | Contract |
56-
| --- | --- |
57-
| `migrateJobDatabase(databaseUrl)` | Explicitly migrate BullMQ's schema and close the migration connection. |
58-
| `createJobQueue(options)` | Open a client for one named PostgreSQL queue. The caller must close it. |
59-
| `enqueueJob(queue, definition, payload, options)` | Validate JSON payload data and enqueue work with job-specific retries and optional delay, priority, or job ID. Failure rejects the call. |
60-
| `createJobProcessor(definition, process, shouldRetry)` | Bind typed execution to runtime payload validation. Invalid stored payloads fail permanently. |
61-
| `createJobWorker(options)` | Construct a stopped consumer for registered processors. The caller starts and closes it. Duplicate processor names are rejected. |
62-
| `upsertJobSchedule(queue, schedule)` | Validate and register an interval or timezone-explicit cron schedule on `scheduled`. |
55+
| Function | Contract |
56+
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
57+
| `migrateJobDatabase(databaseUrl)` | Explicitly migrate BullMQ's schema and close the migration connection. |
58+
| `createJobQueue(options)` | Open a client for one named PostgreSQL queue. The caller must close it. |
59+
| `enqueueJob(queue, definition, payload, options)` | Validate JSON payload data and enqueue work with job-specific retries and optional delay, priority, or job ID. Failure rejects the call. |
60+
| `createJobProcessor(definition, process, shouldRetry)` | Bind typed execution to runtime payload validation. Invalid stored payloads fail permanently. |
61+
| `createJobWorker(options)` | Construct a stopped consumer for registered processors. The caller starts and closes it. Duplicate processor names are rejected. |
62+
| `upsertJobSchedule(queue, schedule)` | Validate and register an interval or timezone-explicit cron schedule on `scheduled`. |
6363

6464
`runJobWorker()` owns process signals and consumer lifetime outside the integration. Integration code does not import domains, repositories, or the shared logger; worker composition injects processors and logging.
6565

0 commit comments

Comments
 (0)