K12 Backend Core (Microservices)
This project is part of the K12 Backend Ecosystem, built with TypeScript, Express.js, and Node.js 22.
It follows a microservices architecture, where each domain is isolated into an independent service.
Currently, the system includes:
- Core Service – user management, reports, investigations
- Auth Service – standalone authentication & authorization microservice (JWT, refresh tokens, sessions)
- Investigations Infrastructure – investigation workflows
Each service communicates via gRPC (synchronous) and Redis Pub/Sub (asynchronous).
Shared contracts (.proto files, DTOs, enums, interfaces) are maintained in a common package.
Requirements
- Node.js v22 or higher
- pnpm for dependency management
- MongoDB for persistence
- Redis for cache and inter-service messaging
- protoc +
ts-protofor gRPC code generation
Core Service Structure
src/
├─ index.ts # entry: starts Express server
├─ app.ts # express app, middlewares, routes
├─ config/
│ ├─ env.ts # environment configuration
│ ├─ logger.ts # centralized logger
│ └─ swagger.ts # Swagger/OpenAPI configuration
├─ docs/ # API documentation (YAML files for Swagger)
│ └─ investigation.routes.yaml
├─ controllers/ # controllers (request handling)
│ ├─ user.controller.ts
│ ├─ report.controller.ts
│ └─ investigation.controller.ts
├─ routes/ # express routes
│ ├─ user.routes.ts
│ ├─ report.routes.ts
│ ├─ investigation.routes.ts
│ └─ swagger.routes.ts # Swagger UI routes
├─ models/ # mongoose models
│ ├─ User.model.ts
│ ├─ Report.model.ts
│ └─ Investigation.model.ts
├─ services/ # business logic
│ ├─ user.service.ts
│ ├─ report.service.ts
│ └─ investigation.service.ts
├─ interfaces/ # service/domain contracts
│ ├─ IUserService.ts
│ ├─ IReportService.ts
│ ├─ IInvestigationService.ts
│ └─ IRepository.ts
├─ middlewares/
│ ├─ errorHandler.ts
│ └─ validateRequest.ts
├─ database/
│ ├─ mongo.connection.ts
│ └─ redis.client.ts
├─ grpc/ # generated gRPC clients
│ ├─ auth.proto
│ ├─ auth_grpc_pb.ts
│ └─ auth_pb.ts
├─ utils/
│ ├─ schema.ts
│ ├─ time.ts
│ └─ apiResponse.ts
├─ types/ # DTOs, enums, global contracts
│ ├─ user.ts # IUser, IUserDTO, UserRole
│ ├─ report.ts # IReport, ReportStatus
│ ├─ investigation.ts # IInvestigation, InvestigationState
│ ├─ enums.ts # ErrorCode, LogLevel
│ └─ index.d.ts # Express global type extensions
├─ tests/
│ ├─ unit/
│ └─ e2e/
Auth Service (separate microservice)
The Auth Service is deployed independently.
It exposes gRPC endpoints for authentication and authorization logic.
gRPC Endpoints
| RPC Method | Maps To Endpoint | Description |
|---|---|---|
GenerateToken(LoginRequest) → TokenResponse |
/auth/login |
Authenticates user and returns JWT token. |
ChangePassword(ChangePasswordRequest) → StatusResponse |
/auth/change-password |
Allows logged-in users to change their password. |
ChangeAvatar(ChangeAvatarRequest) → StatusResponse |
/auth/change-avatar |
Updates user avatar and stores it in GCS. |
CreateUser(CreateUserRequest) → UserResponse |
/admin/create-user |
Admin-only. Creates new user with role and credentials. |
ResetPassword(ResetPasswordRequest) → StatusResponse |
/admin/reset-password |
Admin-only. Resets another user's password. |
DeleteUser(DeleteUserRequest) → StatusResponse |
/admin/delete-user |
Admin-only. Deletes a user (not self). |
GetUserById(UserRequest) → UserResponse |
/me |
Returns info about current authenticated user. |
Inter-Service Communication
- gRPC → synchronous calls (Core ↔ Auth).
- Example: Core calls
ValidateTokenin Auth for protected routes.
- Example: Core calls
- Redis Pub/Sub → asynchronous events (e.g.,
user.created,session.expired).
Physics WebSocket API
The physics engine integration uses Socket.IO for real-time communication with the physics service.
Events Overview
| Direction | Event | Description |
|---|---|---|
| Client → Server | physics:command |
Send commands to physics engine |
| Server → Client | physics:response |
Response to commands |
| Server → Client | physics:state |
Continuous state broadcasts (after simulation starts) |
Command Structure
interface IPhysicsCommand {
type: "health" | "generic" | "start" | "stop" | "pause" | "resume" | "destroy";
investigationId: string;
payload: IGenericCommandPayload | Record<string, unknown>;
}
Response Structure
interface IPhysicsResponse {
success: boolean;
type: PhysicsCommandType;
investigation_id: string;
data: Record<string, unknown> | null;
error: string | null;
error_code: PhysicsErrorCode | null;
error_details: string | null;
}
State Broadcast Structure
interface IPhysicsStateBroadcast {
type: "state";
investigationId: string;
data: {
hasSystem: boolean;
running: boolean;
fps: number;
time: number;
timescale: number;
objectCount: number;
objects: Array<{
id: string;
position: [number, number, number];
rotation: [number, number, number, number]; // quaternion [w, x, y, z]
velocity: [number, number, number];
}>;
};
}
Generic Command Payload
For type: "generic" commands:
interface IGenericCommandPayload {
command: {
op:
| "create"
| "call"
| "get"
| "set"
| "delete"
| "query"
| "step"
| "timescale"
| "clear"
| "callModule";
className?: string; // For "create" op
args?: unknown[]; // Constructor/method arguments
kwargs?: Record<string, unknown>;
assignId?: string; // Custom ID for created object
addToSystem?: boolean; // Auto-add to physics system
target?: string; // Object ID for call/get/set/delete
method?: string; // Method name for "call" op
property?: string; // Property name for get/set
value?: unknown; // Value for "set" op
storeResult?: string; // Store method result with ID
dt?: number; // Time step for "step" op
};
}
Special Value Types
Use these markers for physics engine types:
// Vector
{ __type: "ChVector3", x: 0, y: 0, z: 0 }
// Quaternion
{ __type: "ChQuaternion", w: 1, x: 0, y: 0, z: 0 }
// Reference to existing object
{ __type: "ObjectRef", id: "my_object_id" }
// Reference to the system
{ __type: "SystemRef" }
// Enum value
{ __type: "ChEnum", class: "ChCollisionShape", enum: "Type", value: "BOX" }
Connection Flow Example
const socket = io("wss://your-server.com", {
/* auth config */
});
const investigationId = "inv_12345";
// Helper to send command and wait for response
function sendCommand(command) {
return new Promise((resolve) => {
socket.once("physics:response", resolve);
socket.emit("physics:command", command);
});
}
// 1. Health check (optional)
await sendCommand({ type: "health", investigationId: "", payload: {} });
// 2. Create physics system (required before creating objects)
await sendCommand({
type: "generic",
investigationId,
payload: {
command: {
op: "create",
className: "ChSystemNSC", // or "ChSystemSMC" for smooth contact
assignId: "system",
},
},
});
// 3. Create ground body
await sendCommand({
type: "generic",
investigationId,
payload: {
command: {
op: "create",
className: "ChBodyEasyBox",
args: [10, 1, 10, 1000, true],
assignId: "ground",
addToSystem: true,
},
},
});
// 4. Set ground as fixed
await sendCommand({
type: "generic",
investigationId,
payload: {
command: { op: "call", target: "ground", method: "SetFixed", args: [true] },
},
});
// 5. Create a falling sphere
await sendCommand({
type: "generic",
investigationId,
payload: {
command: {
op: "create",
className: "ChBodyEasySphere",
args: [0.5, 1000, true],
assignId: "sphere",
addToSystem: true,
},
},
});
// 6. Position the sphere
await sendCommand({
type: "generic",
investigationId,
payload: {
command: {
op: "call",
target: "sphere",
method: "SetPos",
args: [{ __type: "ChVector3", x: 0, y: 5, z: 0 }],
},
},
});
// 7. Subscribe to state updates
socket.on("physics:state", (state) => {
console.log(`Time: ${state.data.time.toFixed(3)}s`);
state.data.objects.forEach((obj) => {
console.log(` ${obj.id}: pos=[${obj.position.join(", ")}]`);
});
});
// 8. Start simulation (enables state broadcasts)
await sendCommand({ type: "start", investigationId, payload: {} });
// 9. Control simulation
await sendCommand({ type: "pause", investigationId, payload: {} });
await sendCommand({ type: "resume", investigationId, payload: {} });
// 10. Change timescale (2x speed)
await sendCommand({
type: "generic",
investigationId,
payload: { command: { op: "timescale", value: 2.0 } },
});
// 11. Stop/destroy when done
await sendCommand({ type: "stop", investigationId, payload: {} });
// or
await sendCommand({ type: "destroy", investigationId, payload: {} });
Command Sequence Summary
| Step | Command Type | Purpose |
|---|---|---|
| 1 | health |
Verify physics engine is available |
| 2 | generic (op: create) |
Create physics system (required first) |
| 3 | generic (op: create) |
Create physics bodies/objects |
| 4 | generic (op: call/set) |
Configure objects (position, properties) |
| 5 | start |
Begin simulation loop (enables state broadcasts) |
| 6 | pause/resume |
Control simulation playback |
| 7 | generic (op: step) |
Manual stepping (when paused) |
| 8 | stop or destroy |
End simulation (stops state broadcasts) |
Development Standards
- TypeScript strict mode
- ESLint + Prettier enforced
- JSDoc required for public methods
- Husky pre-commit hooks: lint, type-check, tests
Commit examples:
feat: add sceneLoop placeholderfix: correct ESM import in index.tsdocs: add PROJECT_STRUCTURE.mdrefactor(core): simplify World.tick ordertest(e2e): add ws happy pathbuild: update tsconfig for declarationsci: run lint and test on PRrevert: revert "feat: add ws"
Installation (Core Service)
-
Install Node.js v22
-
Install pnpm globally
npm install -g pnpm -
Clone repository and install deps
git clone <repository-url> cd k12_BackEnd_Core pnpm install -
Setup Husky
pnpm dlx husky install -
Install
protoc- macOS (Homebrew):
brew install protobuf
- macOS (Homebrew):
-
Generate gRPC code from proto files
pnpm proto:gen -
Configure
.envPORT=3000 MONGO_URI=mongodb://localhost:27017/k12 REDIS_URI=localhost REDIS_PORT=6379 AUTH_GRPC_HOST=localhost AUTH_GRPC_PORT=50051 # Google Cloud Storage (for investigation object photos) GCS_BUCKET_NAME=k12-core GCS_PROJECT_ID=your-gcp-project-id ENV_PREFIX=dev -
Setup Google Cloud Storage credentials (for object photo uploads)
- Create a service account in Google Cloud Console with Storage Admin permissions
- Download the service account JSON key file
- Create a
data/directory in the project root:mkdir -p data - Save the JSON key file as
data/gcp-service.json - Ensure
data/is in.gitignore(already configured) - Important: Never commit this file to version control
Scripts
pnpm dev– run dev server (API docs at/api-docs)pnpm build– compile TypeScriptpnpm start– run compiled buildpnpm lint– lint codepnpm proto:gen– generate gRPC stubs from proto filespnpm test– run unit + e2e testspnpm run docs– generate JSDoc HTML docs intodocs/(ignored by git)
Documentation
JSDoc Documentation
Run pnpm run docs to build the temporary .doc-tmp/ bundle and output HTML documentation into docs/. The docs/ directory is ignored by git—regenerate it locally or in CI whenever you need fresh API docs.
API Documentation (Swagger/OpenAPI)
Swagger UI is available at http://localhost:3000/api-docs (or /api-docs on deployed instance).
To authorize requests in Swagger UI, click the lock icon and enter your JWT token.
API documentation is maintained in separate YAML files in src/docs/ to keep route files clean. See SWAGGER.md for details on adding new endpoint documentation.
Testing
- Unit tests – services, utils
- E2E tests – gRPC interactions + API routes