Home

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-proto for 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 ValidateToken in Auth for protected routes.
  • 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 placeholder
  • fix: correct ESM import in index.ts
  • docs: add PROJECT_STRUCTURE.md
  • refactor(core): simplify World.tick order
  • test(e2e): add ws happy path
  • build: update tsconfig for declarations
  • ci: run lint and test on PR
  • revert: revert "feat: add ws"

Installation (Core Service)

  1. Install Node.js v22

  2. Install pnpm globally

    npm install -g pnpm
    
  3. Clone repository and install deps

    git clone <repository-url>
    cd k12_BackEnd_Core
    pnpm install
    
  4. Setup Husky

    pnpm dlx husky install
    
  5. Install protoc

    • macOS (Homebrew):
      brew install protobuf
      
  6. Generate gRPC code from proto files

    pnpm proto:gen
    
  7. Configure .env

    PORT=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
    
  8. 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 TypeScript
  • pnpm start – run compiled build
  • pnpm lint – lint code
  • pnpm proto:gen – generate gRPC stubs from proto files
  • pnpm test – run unit + e2e tests
  • pnpm run docs – generate JSDoc HTML docs into docs/ (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