mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 17:16:19 +00:00
772 lines
24 KiB
Markdown
772 lines
24 KiB
Markdown
# Backend Architecture
|
|
|
|
## Goal
|
|
|
|
This document defines the target backend shape for the TypeScript refactor in `server/src`.
|
|
|
|
The main constraints are:
|
|
|
|
- Keep the current API paths stable while the internals move out of `server/index.js` and `server/routes/*.js`.
|
|
- Migrate one route family at a time instead of doing a big-bang rewrite.
|
|
- Keep controllers thin and move business logic into services and repositories.
|
|
- Keep transport concerns, provider adapters, and domain logic separate.
|
|
|
|
## Current State
|
|
|
|
As of March 18, 2026:
|
|
|
|
- `server/src/bootstrap.ts` starts the TypeScript backend entrypoint.
|
|
- `server/src/app.ts` still bridges into `server/src/runner.ts`.
|
|
- `server/src/shared/*` already contains reusable TypeScript building blocks such as:
|
|
- `shared/http/api-response.ts`
|
|
- `shared/http/async-handler.ts`
|
|
- `shared/http/error-handler.ts`
|
|
- `shared/http/request-context.ts`
|
|
- `shared/utils/app-error.ts`
|
|
- `shared/database/repositories/*`
|
|
- `server/src/modules/*` mostly exist as placeholder folders.
|
|
- The real runtime behavior still mostly lives in:
|
|
- `server/index.js`
|
|
- `server/routes/*.js`
|
|
- `docs/backend/endpoint-inventory.md` is the migration checklist for the existing HTTP surface.
|
|
|
|
## Recommended Target Structure
|
|
|
|
```text
|
|
server/
|
|
index.js # legacy runtime bridge during migration
|
|
start.js # production entrypoint for compiled output
|
|
src/
|
|
bootstrap.ts # starts the backend process
|
|
app.ts # creates the app/server and registers modules
|
|
runner.ts # transitional runtime while legacy code still exists
|
|
config/
|
|
load-env-vars.ts
|
|
runtime.ts
|
|
realtime/
|
|
index.ts # attaches websocket handlers to the HTTP server
|
|
chat.gateway.ts # chat websocket behavior
|
|
shell.gateway.ts # shell websocket behavior
|
|
events.ts # shared broadcast helpers
|
|
shared/
|
|
auth/
|
|
authenticate-token.ts # future TS auth middleware
|
|
validate-api-key.ts # future TS API-key middleware
|
|
database/
|
|
connection.ts
|
|
init-db.ts
|
|
migrations.ts
|
|
schema.ts
|
|
types.ts
|
|
repositories/
|
|
api-keys.ts
|
|
app-config.ts
|
|
credentials.ts
|
|
session-names.ts
|
|
users.ts
|
|
docs/
|
|
openapi.ts
|
|
http/
|
|
api-response.ts
|
|
async-handler.ts
|
|
error-handler.ts
|
|
not-found-handler.ts
|
|
request-context.ts
|
|
input/
|
|
parse-boolean.ts # future shared query/body parsers
|
|
parse-pagination.ts
|
|
platform/
|
|
index.ts
|
|
path.ts
|
|
runtime-platform.ts
|
|
shell.ts
|
|
stream.ts
|
|
text.ts
|
|
types.ts
|
|
types/
|
|
app.ts
|
|
http.ts
|
|
utils/
|
|
app-error.ts
|
|
logger.ts
|
|
modules/
|
|
auth/
|
|
index.ts
|
|
auth.routes.ts
|
|
auth.controller.ts
|
|
auth.service.ts
|
|
auth.schemas.ts
|
|
auth.types.ts
|
|
cli-auth/
|
|
index.ts
|
|
cli-auth.routes.ts
|
|
cli-auth.controller.ts
|
|
cli-auth.service.ts
|
|
user/
|
|
index.ts
|
|
user.routes.ts
|
|
user.controller.ts
|
|
user.service.ts
|
|
user.schemas.ts
|
|
user.types.ts
|
|
settings/
|
|
index.ts
|
|
settings.routes.ts
|
|
settings.controller.ts
|
|
settings.service.ts
|
|
settings.schemas.ts
|
|
commands/
|
|
index.ts
|
|
commands.routes.ts
|
|
commands.controller.ts
|
|
commands.service.ts
|
|
projects/
|
|
index.ts
|
|
projects.routes.ts
|
|
projects.controller.ts
|
|
projects.service.ts
|
|
projects.schemas.ts
|
|
workspace-path.policy.ts
|
|
files/
|
|
index.ts
|
|
files.routes.ts
|
|
files.controller.ts
|
|
files.service.ts
|
|
files.schemas.ts
|
|
sessions/
|
|
index.ts
|
|
sessions.routes.ts
|
|
sessions.controller.ts
|
|
sessions.service.ts
|
|
sessions.schemas.ts
|
|
git/
|
|
index.ts
|
|
git.routes.ts
|
|
git.controller.ts
|
|
git.service.ts
|
|
taskmaster/
|
|
index.ts
|
|
taskmaster.routes.ts
|
|
taskmaster.controller.ts
|
|
taskmaster.service.ts
|
|
agent/
|
|
index.ts
|
|
agent.routes.ts
|
|
agent.controller.ts
|
|
agent.service.ts
|
|
system/
|
|
index.ts
|
|
system.routes.ts
|
|
system.controller.ts
|
|
system.service.ts
|
|
spa-fallback.ts
|
|
providers/
|
|
claude/
|
|
index.ts
|
|
claude.service.ts
|
|
claude-session.service.ts
|
|
codex/
|
|
index.ts
|
|
codex.routes.ts
|
|
codex.controller.ts
|
|
codex.service.ts
|
|
cursor/
|
|
index.ts
|
|
cursor.routes.ts
|
|
cursor.controller.ts
|
|
cursor.service.ts
|
|
gemini/
|
|
index.ts
|
|
gemini.routes.ts
|
|
gemini.controller.ts
|
|
gemini.service.ts
|
|
mcp/
|
|
index.ts
|
|
mcp.routes.ts
|
|
mcp.controller.ts
|
|
mcp.service.ts
|
|
plugins/
|
|
index.ts
|
|
plugins.routes.ts
|
|
plugins.controller.ts
|
|
plugins.service.ts
|
|
testing/
|
|
fixtures/
|
|
```
|
|
|
|
## Why This Shape Works For This Repo
|
|
|
|
- You already have shared TypeScript HTTP/database utilities in `server/src/shared`.
|
|
- The legacy route surface is already grouped by domain in `server/routes/*.js`.
|
|
- The biggest remaining problem is not "what framework should I use", it is "where should each existing endpoint live and how do I migrate it without breaking paths".
|
|
- A module-per-domain structure solves that without forcing a rewrite of all helpers at once.
|
|
|
|
## Route Ownership
|
|
|
|
Keep the public paths stable. Only move the code behind them.
|
|
|
|
| Module | Mount path(s) | Current source(s) | Notes |
|
|
| --- | --- | --- | --- |
|
|
| `auth` | `/api/auth` | `server/routes/auth.js` | Login, register, logout, auth status, current user |
|
|
| `cli-auth` | `/api/cli` | `server/routes/cli-auth.js` | Provider CLI auth status endpoints |
|
|
| `user` | `/api/user` | `server/routes/user.js` | Git identity and onboarding state |
|
|
| `settings` | `/api/settings` | `server/routes/settings.js` | API keys, credentials, notification preferences, push setup |
|
|
| `commands` | `/api/commands` | `server/routes/commands.js` | Slash command list, load, execute |
|
|
| `projects` | `/api/projects`, `/api/browse-filesystem`, `/api/create-folder` | `server/routes/projects.js`, `server/index.js` | Workspace discovery and workspace creation helpers |
|
|
| `files` | `/api/projects/:projectName/file`, `/api/projects/:projectName/files` | `server/index.js` | File tree, read/write, upload, rename, delete |
|
|
| `sessions` | `/api/projects/:projectName/sessions`, `/api/sessions/:sessionId/rename`, `/api/search/conversations` | `server/index.js`, provider route files | Project/provider session history |
|
|
| `git` | `/api/git` | `server/routes/git.js` | Repo operations and git automation |
|
|
| `taskmaster` | `/api/taskmaster` | `server/routes/taskmaster.js` | TaskMaster setup, PRDs, tasks, parsing |
|
|
| `agent` | `/api/agent` | `server/routes/agent.js` | External agent orchestration |
|
|
| `system` | `/health`, `/api/system`, `/api/transcribe`, SPA fallback | `server/index.js` | Health, app update, transcription, non-API fallback. Register the SPA fallback last. |
|
|
| `providers/codex` | `/api/codex` | `server/routes/codex.js` | Config, MCP-adjacent behavior, Codex sessions |
|
|
| `providers/cursor` | `/api/cursor` | `server/routes/cursor.js` | Config, MCP, Cursor sessions |
|
|
| `providers/gemini` | `/api/gemini` | `server/routes/gemini.js` | Gemini session endpoints |
|
|
| `providers/mcp` | `/api/mcp`, `/api/mcp-utils` | `server/routes/mcp.js`, `server/routes/mcp-utils.js` | Shared MCP HTTP endpoints |
|
|
| `providers/plugins` | `/api/plugins` | `server/routes/plugins.js` | Plugin install, enable/disable, assets |
|
|
| `providers/claude` | no standalone HTTP prefix yet | `server/index.js`, `server/claude-sdk.js` | Service/adapters used by chat and agent flows |
|
|
| `realtime` | `/ws`, `/shell` | `server/index.js` | Websocket transport, not HTTP routes |
|
|
|
|
## Module Convention
|
|
|
|
Every feature module should follow the same internal shape:
|
|
|
|
```text
|
|
modules/user/
|
|
index.ts # registers the module on the app
|
|
user.routes.ts # express.Router + middleware order
|
|
user.controller.ts # request/response mapping only
|
|
user.service.ts # business rules and orchestration
|
|
user.schemas.ts # input parsing and validation
|
|
user.types.ts # module-local types only
|
|
```
|
|
|
|
### Responsibilities
|
|
|
|
- `index.ts`
|
|
Owns the mount path.
|
|
`app.ts` should not know route internals.
|
|
|
|
- `*.routes.ts`
|
|
Declares routes, middleware order, and child-router mounting.
|
|
It should not contain SQL, filesystem logic, or long validation blocks.
|
|
|
|
- `*.controller.ts`
|
|
Reads `req`, calls schema parsers and services, then writes the response.
|
|
It should stay thin.
|
|
|
|
- `*.service.ts`
|
|
Owns business logic.
|
|
It can call repositories, filesystem helpers, platform helpers, provider adapters, and child processes.
|
|
|
|
- `*.schemas.ts`
|
|
Validates/parses request data close to the module.
|
|
Throw `AppError` with `400` status for bad input.
|
|
|
|
- `*.types.ts`
|
|
Holds module-local types that do not belong in `shared/types`.
|
|
|
|
## Application Setup
|
|
|
|
The current `server/src/app.ts` still starts a transitional runner.
|
|
Once the HTTP runtime moves into `server/src`, the shape should look like this:
|
|
|
|
```ts
|
|
import cors from 'cors';
|
|
import express from 'express';
|
|
import http from 'http';
|
|
|
|
import { registerAgentModule } from '@/modules/agent/index.js';
|
|
import { registerAuthModule } from '@/modules/auth/index.js';
|
|
import { registerCliAuthModule } from '@/modules/cli-auth/index.js';
|
|
import { registerCommandsModule } from '@/modules/commands/index.js';
|
|
import { registerFilesModule } from '@/modules/files/index.js';
|
|
import { registerGitModule } from '@/modules/git/index.js';
|
|
import { registerProjectsModule } from '@/modules/projects/index.js';
|
|
import { registerSessionsModule } from '@/modules/sessions/index.js';
|
|
import { registerSettingsModule } from '@/modules/settings/index.js';
|
|
import { registerSystemModule } from '@/modules/system/index.js';
|
|
import { registerTaskmasterModule } from '@/modules/taskmaster/index.js';
|
|
import { registerUserModule } from '@/modules/user/index.js';
|
|
import { registerCodexProviderModule } from '@/modules/providers/codex/index.js';
|
|
import { registerCursorProviderModule } from '@/modules/providers/cursor/index.js';
|
|
import { registerGeminiProviderModule } from '@/modules/providers/gemini/index.js';
|
|
import { registerMcpProviderModule } from '@/modules/providers/mcp/index.js';
|
|
import { registerPluginsProviderModule } from '@/modules/providers/plugins/index.js';
|
|
import { registerSpaFallback } from '@/modules/system/spa-fallback.js';
|
|
import { errorHandler } from '@/shared/http/error-handler.js';
|
|
import { notFoundHandler } from '@/shared/http/not-found-handler.js';
|
|
import { requestContextMiddleware } from '@/shared/http/request-context.js';
|
|
import { attachRealtimeHandlers } from '@/realtime/index.js';
|
|
|
|
export function createHttpRuntime() {
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
|
|
// Global middleware should be registered once, before feature modules.
|
|
app.use(cors());
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.urlencoded({ extended: true }));
|
|
app.use(requestContextMiddleware);
|
|
|
|
// Keep health/system API routes early so diagnostics still work
|
|
// even if a later feature module throws during registration.
|
|
registerSystemModule(app);
|
|
|
|
// Public/auth routes first.
|
|
registerAuthModule(app);
|
|
|
|
// Authenticated feature modules after auth.
|
|
registerCliAuthModule(app);
|
|
registerUserModule(app);
|
|
registerSettingsModule(app);
|
|
registerCommandsModule(app);
|
|
registerProjectsModule(app);
|
|
registerFilesModule(app);
|
|
registerSessionsModule(app);
|
|
registerGitModule(app);
|
|
registerTaskmasterModule(app);
|
|
registerAgentModule(app);
|
|
registerCodexProviderModule(app);
|
|
registerCursorProviderModule(app);
|
|
registerGeminiProviderModule(app);
|
|
registerMcpProviderModule(app);
|
|
registerPluginsProviderModule(app);
|
|
|
|
// Websocket setup should live outside HTTP controllers.
|
|
attachRealtimeHandlers(server, app);
|
|
|
|
// The React fallback must be last or it will swallow real API routes.
|
|
registerSpaFallback(app);
|
|
|
|
app.use(notFoundHandler);
|
|
app.use(errorHandler);
|
|
|
|
return { app, server };
|
|
}
|
|
```
|
|
|
|
## Example Module: `user`
|
|
|
|
This is a good first migration candidate because the endpoints are small, the domain is clear, and you already have a typed user repository in `server/src/shared/database/repositories/users.ts`.
|
|
|
|
### Folder
|
|
|
|
```text
|
|
server/src/modules/user/
|
|
index.ts
|
|
user.routes.ts
|
|
user.controller.ts
|
|
user.service.ts
|
|
user.schemas.ts
|
|
user.types.ts
|
|
```
|
|
|
|
### `index.ts`
|
|
|
|
```ts
|
|
import type { Express } from 'express';
|
|
|
|
import { createUserRouter } from './user.routes.js';
|
|
|
|
export function registerUserModule(app: Express): void {
|
|
// Keep the mount path in one place.
|
|
// This makes it easy to see who owns `/api/user`.
|
|
app.use('/api/user', createUserRouter());
|
|
}
|
|
```
|
|
|
|
### `user.routes.ts`
|
|
|
|
```ts
|
|
import { Router } from 'express';
|
|
|
|
import { asyncHandler } from '@/shared/http/async-handler.js';
|
|
// Day 1: reuse the existing JS middleware while auth is still being migrated.
|
|
import { authenticateToken } from '../../../middleware/auth.js';
|
|
|
|
import {
|
|
completeOnboardingController,
|
|
getGitConfigController,
|
|
getOnboardingStatusController,
|
|
updateGitConfigController,
|
|
} from './user.controller.js';
|
|
|
|
export function createUserRouter(): Router {
|
|
const router = Router();
|
|
|
|
// Routes only describe the HTTP surface and middleware order.
|
|
router.get('/git-config', authenticateToken, asyncHandler(getGitConfigController));
|
|
router.post('/git-config', authenticateToken, asyncHandler(updateGitConfigController));
|
|
router.get('/onboarding-status', authenticateToken, asyncHandler(getOnboardingStatusController));
|
|
router.post('/complete-onboarding', authenticateToken, asyncHandler(completeOnboardingController));
|
|
|
|
return router;
|
|
}
|
|
```
|
|
|
|
### `user.schemas.ts`
|
|
|
|
```ts
|
|
import { AppError } from '@/shared/utils/app-error.js';
|
|
|
|
export type UpdateGitConfigInput = {
|
|
gitName: string;
|
|
gitEmail: string;
|
|
};
|
|
|
|
export function parseUpdateGitConfigInput(body: unknown): UpdateGitConfigInput {
|
|
const value = body as Partial<UpdateGitConfigInput> | null;
|
|
|
|
// Keep input validation close to the module so controllers stay thin.
|
|
if (!value?.gitName || !value?.gitEmail) {
|
|
throw new AppError('Git name and email are required', {
|
|
code: 'VALIDATION_ERROR',
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
const gitName = value.gitName.trim();
|
|
const gitEmail = value.gitEmail.trim();
|
|
|
|
if (!gitName) {
|
|
throw new AppError('Git name is required', {
|
|
code: 'VALIDATION_ERROR',
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(gitEmail)) {
|
|
throw new AppError('Invalid email format', {
|
|
code: 'VALIDATION_ERROR',
|
|
statusCode: 400,
|
|
});
|
|
}
|
|
|
|
return {
|
|
gitName,
|
|
gitEmail,
|
|
};
|
|
}
|
|
```
|
|
|
|
### `user.service.ts`
|
|
|
|
```ts
|
|
import { userDb } from '@/shared/database/repositories/users.js';
|
|
import { AppError } from '@/shared/utils/app-error.js';
|
|
|
|
import type { UpdateGitConfigInput } from './user.schemas.js';
|
|
|
|
export const userService = {
|
|
getGitConfig(userId: number) {
|
|
const gitConfig = userDb.getGitConfig(userId);
|
|
|
|
return {
|
|
gitName: gitConfig?.git_name ?? null,
|
|
gitEmail: gitConfig?.git_email ?? null,
|
|
};
|
|
},
|
|
|
|
updateGitConfig(userId: number, input: UpdateGitConfigInput) {
|
|
if (!userDb.getUserById(userId)) {
|
|
throw new AppError('User not found', {
|
|
code: 'USER_NOT_FOUND',
|
|
statusCode: 404,
|
|
});
|
|
}
|
|
|
|
// Services own business rules and side effects.
|
|
// If you later re-add `git config --global`, it belongs here.
|
|
userDb.updateGitConfig(userId, input.gitName, input.gitEmail);
|
|
|
|
return {
|
|
gitName: input.gitName,
|
|
gitEmail: input.gitEmail,
|
|
};
|
|
},
|
|
|
|
getOnboardingStatus(userId: number) {
|
|
return {
|
|
hasCompletedOnboarding: userDb.hasCompletedOnboarding(userId),
|
|
};
|
|
},
|
|
|
|
completeOnboarding(userId: number) {
|
|
userDb.completeOnboarding(userId);
|
|
|
|
return {
|
|
message: 'Onboarding completed successfully',
|
|
};
|
|
},
|
|
};
|
|
```
|
|
|
|
### `user.controller.ts`
|
|
|
|
```ts
|
|
import type { Response } from 'express';
|
|
|
|
import {
|
|
createApiMeta,
|
|
createApiSuccessResponse,
|
|
} from '@/shared/http/api-response.js';
|
|
import { getRequestContext } from '@/shared/http/request-context.js';
|
|
import type { AuthenticatedRequest } from '@/shared/types/http.js';
|
|
import { AppError } from '@/shared/utils/app-error.js';
|
|
|
|
import { parseUpdateGitConfigInput } from './user.schemas.js';
|
|
import { userService } from './user.service.js';
|
|
|
|
function getUserId(req: AuthenticatedRequest): number {
|
|
const userId = Number(req.user?.id);
|
|
|
|
if (!Number.isFinite(userId)) {
|
|
throw new AppError('Authenticated user is missing', {
|
|
code: 'UNAUTHENTICATED',
|
|
statusCode: 401,
|
|
});
|
|
}
|
|
|
|
return userId;
|
|
}
|
|
|
|
function getMeta(req: AuthenticatedRequest) {
|
|
const context = getRequestContext(req);
|
|
return createApiMeta(context?.requestId, context?.startedAt);
|
|
}
|
|
|
|
export async function getGitConfigController(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
const data = userService.getGitConfig(getUserId(req));
|
|
res.json(createApiSuccessResponse(data, getMeta(req)));
|
|
}
|
|
|
|
export async function updateGitConfigController(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
const input = parseUpdateGitConfigInput(req.body);
|
|
const data = userService.updateGitConfig(getUserId(req), input);
|
|
res.json(createApiSuccessResponse(data, getMeta(req)));
|
|
}
|
|
|
|
export async function getOnboardingStatusController(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
const data = userService.getOnboardingStatus(getUserId(req));
|
|
res.json(createApiSuccessResponse(data, getMeta(req)));
|
|
}
|
|
|
|
export async function completeOnboardingController(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
const data = userService.completeOnboarding(getUserId(req));
|
|
res.json(createApiSuccessResponse(data, getMeta(req)));
|
|
}
|
|
```
|
|
|
|
## Nested Route Example: `files`
|
|
|
|
`files` is the best example of why route ownership and mount paths matter.
|
|
The files module should stay separate from `projects`, but its routes still need access to `:projectName`.
|
|
|
|
### `files/index.ts`
|
|
|
|
```ts
|
|
import type { Express } from 'express';
|
|
|
|
import { createFilesRouter } from './files.routes.js';
|
|
|
|
export function registerFilesModule(app: Express): void {
|
|
// The files module owns project-scoped file operations.
|
|
app.use('/api/projects/:projectName', createFilesRouter());
|
|
}
|
|
```
|
|
|
|
### `files.routes.ts`
|
|
|
|
```ts
|
|
import { Router } from 'express';
|
|
|
|
import { asyncHandler } from '@/shared/http/async-handler.js';
|
|
import { authenticateToken } from '../../../middleware/auth.js';
|
|
|
|
import {
|
|
deleteFilesController,
|
|
getFileController,
|
|
getFilesController,
|
|
putFileController,
|
|
} from './files.controller.js';
|
|
|
|
export function createFilesRouter(): Router {
|
|
// `mergeParams: true` lets this router read `req.params.projectName`
|
|
// from the mount path declared in `index.ts`.
|
|
const router = Router({ mergeParams: true });
|
|
|
|
router.get('/file', authenticateToken, asyncHandler(getFileController));
|
|
router.put('/file', authenticateToken, asyncHandler(putFileController));
|
|
router.get('/files', authenticateToken, asyncHandler(getFilesController));
|
|
router.delete('/files', authenticateToken, asyncHandler(deleteFilesController));
|
|
|
|
return router;
|
|
}
|
|
```
|
|
|
|
### Why This Separation Is Useful
|
|
|
|
- `projects` decides what a project/workspace is.
|
|
- `files` only operates inside a resolved project.
|
|
- `sessions` only deals with chat/session history.
|
|
- This prevents one giant `projects.service.ts` from becoming the new monolith.
|
|
|
|
## Provider Module Pattern
|
|
|
|
Provider modules should follow the same structure, but they usually own adapter logic as well as HTTP routes.
|
|
|
|
Example shape for `providers/codex`:
|
|
|
|
```text
|
|
modules/providers/codex/
|
|
index.ts
|
|
codex.routes.ts
|
|
codex.controller.ts
|
|
codex.service.ts
|
|
codex.types.ts
|
|
codex-mcp.service.ts
|
|
codex-sessions.service.ts
|
|
```
|
|
|
|
Use this rule:
|
|
|
|
- If the code is specific to one provider, keep it inside that provider folder.
|
|
- If the code is shared across multiple providers, move it into `shared/` or `providers/mcp/`.
|
|
- If the code is an external-facing orchestration endpoint that can call different providers, keep it in `modules/agent/`.
|
|
|
|
## Realtime Structure
|
|
|
|
Do not leave websocket logic inside `app.ts` or feature controllers.
|
|
|
|
Use a dedicated `server/src/realtime/` area:
|
|
|
|
```text
|
|
server/src/realtime/
|
|
index.ts
|
|
chat.gateway.ts
|
|
shell.gateway.ts
|
|
events.ts
|
|
```
|
|
|
|
Suggested responsibilities:
|
|
|
|
- `index.ts`
|
|
Attaches websocket handlers to the HTTP server.
|
|
|
|
- `chat.gateway.ts`
|
|
Owns `/ws` message handling, provider session streaming, reconnect behavior, and approval flows.
|
|
|
|
- `shell.gateway.ts`
|
|
Owns `/shell` PTY session lifecycle, resize events, and output streaming.
|
|
|
|
- `events.ts`
|
|
Owns broadcast helpers used by HTTP modules such as TaskMaster or project updates.
|
|
|
|
## Boundary Rules
|
|
|
|
- `app.ts` wires modules together. It should not contain feature logic.
|
|
- `bootstrap.ts` starts the process. It should not know route details.
|
|
- `routes` files should not contain SQL, filesystem logic, provider process spawning, or long validation blocks.
|
|
- `controllers` should not import `better-sqlite3`, `fs`, `node-pty`, or raw provider SDKs.
|
|
- `services` own business rules, orchestration, and side effects.
|
|
- `repositories` own SQL only.
|
|
- `shared/*` is only for code used by two or more modules.
|
|
- `providers/*` is for provider-specific behavior.
|
|
- `agent` is orchestration above provider modules.
|
|
- `realtime/*` owns websocket transport, not HTTP modules.
|
|
- `system` owns health/update/fallback endpoints, not project/file/session behavior.
|
|
|
|
## Migration Order
|
|
|
|
This order keeps risk low:
|
|
|
|
1. `auth`, `cli-auth`, `user`
|
|
Small route files, easy to validate, minimal path behavior.
|
|
|
|
2. `settings`, `commands`
|
|
Still mostly isolated and already grouped in dedicated route files.
|
|
|
|
3. `projects`, `files`, `sessions`
|
|
These are more coupled and require clear boundaries around `projectName`.
|
|
|
|
4. `git`, `taskmaster`
|
|
Heavier side effects and more external process behavior.
|
|
|
|
5. `providers/*`
|
|
Each provider can move independently once shared platform helpers are stable.
|
|
|
|
6. `agent`
|
|
Migrate last because it orchestrates several other pieces.
|
|
|
|
7. `realtime/*`
|
|
Move websocket logic after the supporting services are already modular.
|
|
|
|
## Day 1 Migration Rules
|
|
|
|
- Keep path compatibility first.
|
|
- Move one route family at a time.
|
|
- It is acceptable to import legacy JS middleware or helpers from a new TS module temporarily.
|
|
- When a route family is migrated:
|
|
- register the new TS module in `app.ts`
|
|
- remove the old route registration from `server/index.js`
|
|
- keep the response shape unchanged unless you intentionally version it
|
|
- Prefer moving business logic into services first, then move transport code.
|
|
|
|
## Practical Setup Checklist
|
|
|
|
When you create a new module, use this checklist:
|
|
|
|
1. Add the folder under `server/src/modules/<feature>/`.
|
|
2. Create `index.ts`, `*.routes.ts`, `*.controller.ts`, `*.service.ts`, and `*.schemas.ts`.
|
|
3. Keep the mount path inside `index.ts`.
|
|
4. Reuse `asyncHandler`, `AppError`, `createApiSuccessResponse`, and `requestContextMiddleware`.
|
|
5. Reuse typed repositories from `server/src/shared/database/repositories/*` whenever possible.
|
|
6. Register the module in `server/src/app.ts`.
|
|
7. Move one legacy route at a time and verify the response shape still matches the frontend.
|
|
|
|
## Package Script Notes
|
|
|
|
These scripts already match the refactor workflow:
|
|
|
|
- `npm run server:dev`
|
|
Use for backend-only refactor work in `server/src`.
|
|
|
|
- `npm run typecheck:server`
|
|
Run after every backend module migration.
|
|
|
|
- `npm run test:server`
|
|
Use for shared platform tests and expand it as new TS modules gain tests.
|
|
|
|
- `npm run server:build`
|
|
Confirms the refactored backend compiles into `server/dist`.
|
|
|
|
- `npm run verify:server`
|
|
Best validation command before merging backend refactor work.
|
|
|
|
## Summary
|
|
|
|
The simplest stable rule set for this refactor is:
|
|
|
|
- keep API paths the same
|
|
- move code into domain modules
|
|
- keep routes thin
|
|
- keep business logic in services
|
|
- keep SQL in repositories
|
|
- keep provider code inside provider folders
|
|
- keep websocket code outside HTTP modules
|
|
|
|
If you follow that consistently, `server/src` will become the real backend instead of a second monolith with TypeScript syntax.
|