Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hub/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Optional API port. The Vite development proxy expects the default.
PORT=3001

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

# Local development only. The devcontainer supplies its own database hostname.
DATABASE_URL=postgresql://replica:replica@127.0.0.1:5432/replica

Expand Down
2 changes: 1 addition & 1 deletion hub/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
log
/log
.local
.npm
.storybook/instances
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,11 +469,13 @@ describe("App()", () => {
await waitFor(() => expect(annotate).toBeEnabled())
const post = vi.spyOn(frame.contentWindow!, "postMessage")
fireEvent.click(annotate)
await waitFor(() => expect(annotate).toHaveAttribute("aria-pressed", "true"))
expect(post).toHaveBeenCalledWith(
{ type: "replica:canvas:select", nonce, mode: "improvement" },
previewOrigin,
await waitFor(() =>
expect(post).toHaveBeenCalledWith(
{ type: "replica:canvas:select", nonce, mode: "improvement" },
previewOrigin,
),
)
expect(annotate).toHaveAttribute("aria-pressed", "true")
expect(screen.queryByRole("complementary", { name: copy.notes })).not.toBeInTheDocument()

receive(frame, { type: "replica:canvas:selected", anchor }, "https://unrelated.example")
Expand Down
1 change: 1 addition & 0 deletions hub/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Hub is being rebuilt. The [Hub overview](../README.md) describes what the baseli
- [Coding conventions](coding-conventions.md): frontend layers and interface rules.
- [Development](development.md): local API and frontend setup.
- [Database](database.md): Prisma schema conventions, migrations, and the shared client.
- [Logging](logging.md): application events, automatic request records, and payload safety.
- [Testing and operations](testing-and-operations.md): baseline checks and their limits.
- [Storybook](storybook.md): retained presentation, fixtures, and screenshot evidence.
- [Prototype canvas](prototype-canvas.md): the separately retained extension.
Expand Down
4 changes: 3 additions & 1 deletion hub/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ Contributors can use PostgreSQL and Prisma locally or in the devcontainer. The d

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.

The only route is `GET /api/health`, which returns `{ "status": "ok" }`. Every other path returns a JSON 404 response. `createApp()` builds the Express app without starting a server. `server/index.ts` starts it at a local address.
The only route is `GET /api/health`, which returns `{ "status": "ok" }`. 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.

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.

The [Pino integration](logging.md) writes structured application and HTTP logs to stdout. `createApp()` installs request logging and JSON parsing before routes, so new endpoints inherit completion records and server-generated request IDs without explicit logging calls.

## Frontend

```mermaid
Expand Down
11 changes: 9 additions & 2 deletions hub/docs/backend-architecture.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# Code architecture conventions

Organize backend functions into repositories, integrations, utilities, and domains. Each layer has a clear job and rules for which other layers it may use.
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.

| Layer | Responsibility | Directory example |
| --- | --- | --- |
| Routes | Parse HTTP requests, call backend functions, and send responses. Directories follow URL paths and HTTP methods. | `routes/api/health/get/index.ts` |
| Repositories | Functions that interact with the database. Each repository directory represents one RESTful resource. | `repositories/user/get-user/index.ts` |
| Integrations | Functions that interact with an API or SDK. Each integration directory represents one API or SDK service. | `integrations/github/get-user/index.ts` |
| Utilities | General functions reusable elsewhere in the codebase. | `utils/format-timestamp/index.ts` |
Expand All @@ -13,21 +14,27 @@ Organize backend functions into repositories, integrations, utilities, and domai

```mermaid
flowchart LR
Routes --> Domains
Routes --> Repositories
Routes --> Integrations
Routes --> Utilities
Domains --> Repositories
Domains --> Integrations
Domains --> Utilities
Repositories --> Utilities
Integrations --> Utilities
```

Repositories and integrations may import only utilities from elsewhere in the backend. Utilities may import only other utilities. Domains may import any of these layers, including other domains. Each layer may also use built-in libraries and npm packages.
Routes may import domains, repositories, integrations, and utilities. Repositories and integrations may import only utilities from elsewhere in the backend. Utilities may import only other utilities. Domains may import any of these backend layers, including other domains, but not routes. Each layer may also use built-in libraries and npm packages.

These rules apply to imported types as well as functions. A repository must not call another repository or an integration. An integration must not call a repository. Keep business decisions in domains rather than moving them into utilities to get around these rules.

## Directory structure

Put each function in a directory named with lowercase words and hyphens, such as `get-user`. Keep `index.ts`, its tests, and its types together. Group repository functions by resource and integration functions by API or SDK service, as shown above. Import from each function's directory, not from a shared file that re-exports its neighbors.

Export route handlers by HTTP method, such as `get()`, and alias them to descriptive `handle*` names in `routes/index.ts`. Its `routes()` function registers endpoints; `createApp()` installs shared middleware and error handling. Express-specific helpers belong in `integrations/express`, not utilities.

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

## Database access
Expand Down
47 changes: 47 additions & 0 deletions hub/docs/logging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# API logging

Hub writes one JSON object per line to stdout through `server/integrations/pino/index.ts`. `LOG_LEVEL` sets the minimum severity: `trace`, `debug`, `info` (default), `warn`, `error`, `fatal`, or `silent`. Invalid values prevent startup.

Logs can still contain private information after redaction. Operators must restrict access and configure collection and retention separately.

## Application events

Use `log()` for application events:

```ts
import { log } from "../integrations/pino/log/index.ts"

log("info", "Build imported", { buildId })
```

Keep messages short and static. Pass data as structured fields, not message text. Redaction matches keys; it cannot detect secrets in free text, including messages and stacks.

In handlers, use `request.log.info({ buildId }, "Build imported")` to include the request ID, or pass `request.log` as the fourth argument to `log()`.

Follow the [import boundaries](backend-architecture.md#import-boundaries). Repositories and other integrations must receive a logger rather than import Pino's shared instance. Never put payloads in `logger.child()` or `setBindings()`: those fields bypass sanitization.

## Automatic HTTP records

`createApp()` installs request logging before JSON parsing and routes. Every endpoint inherits it. Each request gets a server-generated UUID in the `X-Request-Id` response header; incoming IDs are ignored.

Each request produces one final record with `reqId`, `method`, `route`, `statusCode`, `responseTime` (milliseconds), and `outcome`. Responses below 400 use `info`, 4xx use `warn`, and 5xx or connection errors use `error`. Interrupted responses have `outcome: "aborted"` and `statusCode: null`.

`route` is the handler's template, such as `/builds/:id`, or `null` when unmatched. Mounted routers use a relative template to avoid exposing actual mount values. Sanitized parameters, query fields, and parsed JSON bodies are copied when headers are sent, before Express clears route data.

Automatic records exclude raw URLs, headers, cookies, uploads, response bodies, and error details. JSON parsing uses Express's default 100 KiB limit; rejected bodies are not logged. The Express error handler returns generic JSON errors, or closes the connection if headers were sent, without exposing raw errors.

For private free text or other sensitive payloads, exclude all payload fields before sending a response:

```ts
response.locals.omitLogPayload = true
```

This omits `params`, `query`, and `body` while keeping request metadata.

## Structured-field safety

Application fields, `request.log` fields, and automatic payloads share the same sanitizer. It copies data without changing the request or invoking getters or custom serialization methods.

Keys containing password, passwd, passphrase, secret, token, authorization, cookie, API key, private key, or session ID become `[REDACTED]`. Matching ignores case and punctuation and includes nested objects and arrays. Exclude payloads whose sensitive fields do not match these rules.

Structured fields have [size, depth, and entry limits](../server/utils/logging/sanitize-log-value/constants.ts). Markers replace truncated data, cycles, getters, and unsupported values. These limits do not apply to messages or custom bindings and do not replace endpoint validation or privacy decisions.
142 changes: 142 additions & 0 deletions hub/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
"@primer/react-brand": "0.78.0",
"dotenv": "18.0.8",
"express": "5.3.0",
"pino": "10.4.0",
"pino-http": "11.0.0",
"react": "19.3.0",
"react-dom": "19.3.0",
"zod": "4.6.5"
Expand Down
Loading
Loading