API Contracts
- Base URL:
https://foot-factory-api.dotevolve.net/api/v1
Authentication
- Method: JWT Bearer tokens
- Header:
Authorization: Bearer <token> - Session persistence: httpOnly refresh cookie (
GET /auth/refresh) - Tenancy: The JWT payload contains
tenantId. ThetenantMiddlewareautomatically scopes all Mongoose queries to{ tenantId: req.tenantId }, preventing cross-tenant data access.
Standard Response Format
Success:
{ "status": "success", "data": { ... } }
Error:
{ "status": "error", "message": "Human readable description" }
Multi-Tenancy Model
Every resource in the system (workers, articles, jobs, ledger entries, etc.) belongs to exactly one tenant. Tenant isolation is enforced at the middleware layer — not in individual route handlers.
How it works
- The client sends a JWT in the
Authorization: Bearer <token>header. tenantMiddlewaredecodes the JWT and extractstenantId.- All Mongoose queries in that request are automatically scoped to
{ tenantId: req.tenantId }. - Super-admin routes bypass tenant scoping and can access all tenants.
Tenant ID in the JWT
{
"id": "user-object-id",
"tenantId": "tenant-object-id",
"role": "admin",
"iat": 1700000000,
"exp": 1700086400
}
Cross-tenant data access is structurally impossible for non-super-admin users — the middleware injects tenantId before any route handler runs.
Auth Endpoints (/api/v1/auth)
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
| POST | /auth/login |
No | — | Login with username + password. Returns JWT + sets refresh cookie. |
| GET | /auth/refresh |
Cookie | — | Refresh JWT using httpOnly cookie. Returns new token + user. |
| GET | /auth/logout |
No | — | Clears the refresh cookie. |
| POST | /auth/signup |
Yes | admin | Create a new user within the caller's tenant. |
| POST | /auth/forgotPassword |
No | — | Sends password reset email. |
| PATCH | /auth/resetPassword/:token |
No | — | Resets password using token from email. |
Login Request
{ "username": "admin@example.com", "password": "secret" }
Login / Refresh Response
{
"status": "success",
"token": "<jwt>",
"data": {
"user": { "_id": "...", "name": "...", "email": "...", "role": "admin" }
}
}
Authentication Flow
sequenceDiagram
participant Client as Admin/Tenant Client
participant API as Core API
participant DB as MongoDB
Client->>API: POST /auth/login (username, password)
API->>DB: Verify credentials
DB-->>API: User matched
API-->>Client: 200 OK + JWT (Body) + refreshToken (httpOnly Cookie)
note over Client,API: Subsequent Requests
Client->>API: GET /tenant (Header: Auth Bearer JWT)
API-->>Client: 200 OK (Data)
note over Client,API: Token Expiry Flow
Client->>API: GET /tenant (Header: Auth Bearer expired JWT)
API-->>Client: 401 Unauthorized
Client->>API: GET /auth/refresh (Sends httpOnly Cookie implicitly)
API->>DB: Verify refresh token
DB-->>API: Valid session
API-->>Client: 200 OK + New JWT (Body)
Step 1 — Acquire a token
POST /auth/login
{ "username": "admin@example.com", "password": "secret" }
Response sets an httpOnly refreshToken cookie and returns a short-lived JWT:
{
"status": "success",
"token": "<jwt>",
"data": {
"user": { "_id": "...", "name": "...", "email": "...", "role": "admin" }
}
}
Step 2 — Use the token
Include the JWT in every subsequent request:
Authorization: Bearer <jwt>
Step 3 — Refresh the token
When the JWT expires (401 response), call:
GET /auth/refresh
The httpOnly cookie is sent automatically. Returns a new JWT. The admin client handles this automatically via an Axios interceptor.
Step 4 — Logout
GET /auth/logout
Clears the httpOnly refresh cookie. The client should also discard the JWT from memory.
Error Responses
| Status | Meaning |
|---|---|
| 400 | Missing or invalid fields |
| 401 | Invalid credentials or expired token |
| 403 | Role not permitted (e.g. non-super-admin login to admin panel) |
Tenant Endpoints (/api/v1/tenant)
All routes require super-admin role.
| Method | Path | Description |
|---|---|---|
| GET | /tenant |
List all tenants |
| POST | /tenant |
Provision a new tenant (atomic — creates tenant + admin user) |
| GET | /tenant/stats |
Aggregate tenant counts grouped by status |
| GET | /tenant/:id |
Get a single tenant |
| PATCH | /tenant/:id |
Update tenant fields |
| PATCH | /tenant/:id/status |
Suspend or reactivate a tenant { status: 'suspended' \| 'active' } |
| POST | /tenant/:id/retry-domain |
Retry subdomain provisioning for a tenant with domainStatus: "pending" |
| DELETE | /tenant/:id |
Delete a tenant |
Provision Tenant Request
{
"name": "Acme Corp",
"slug": "acme-corp",
"plan": "professional",
"adminName": "John Admin",
"adminEmail": "john@acme.com",
"adminPassword": "securepass",
"adminConfirmPassword": "securepass"
}
The plan value must match an active Plan slug in the database. The tenant's settings.maxUsers and settings.maxWorkers are automatically populated from the plan limits at provisioning time.
Provision Tenant Responses
201 Created — tenant created and subdomain fully provisioned:
{
"status": "success",
"data": {
"tenant": {
"_id": "...",
"name": "Acme Corp",
"slug": "acme-corp",
"domain": "acme-corp.dotevolve.net",
"domainStatus": "active"
}
}
}
207 Multi-Status — tenant created but subdomain provisioning partially or fully failed:
{
"status": "partial",
"data": {
"tenant": {
"_id": "...",
"name": "Acme Corp",
"slug": "acme-corp",
"domain": "acme-corp.dotevolve.net",
"domainStatus": "pending"
}
},
"provisioningResult": {
"subdomain": "acme-corp.dotevolve.net",
"vercel": { "status": "failed", "error": "Unauthorized" },
"cloudflare": { "status": "success" }
}
}
A 207 means the tenant is fully created in the database — only the DNS/Vercel setup is pending. The super admin can retry via POST /tenant/:id/retry-domain.
Retry Domain Request
POST /tenant/:id/retry-domain — no request body required.
Response (200):
{
"status": "success",
"data": { "tenant": { "domainStatus": "active", ... } },
"provisioningResult": {
"subdomain": "acme-corp.dotevolve.net",
"vercel": { "status": "success" },
"cloudflare": { "status": "success" }
}
}
Returns 400 if domainStatus is already "active". Returns 404 if tenant not found.
Error Responses
| Status | Meaning |
|---|---|
| 207 | Tenant created but subdomain provisioning partially failed |
| 400 | Validation error (missing required fields, invalid plan slug) |
| 404 | Tenant not found |
| 409 | Slug already in use |
| 500 | Unexpected server error |
Tenant Config Endpoints (/api/v1/tenant-config)
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
| POST | /tenant-config |
Yes | super-admin | Upsert (create or update) a tenant's schema config |
| GET | /tenant-config/:tenantId |
Yes | any | Fetch a tenant's schema config |
Upsert Config Request
{
"tenantId": "5f9c...",
"jobSteps": [
{ "name": "Cutting", "order": 1, "isPaid": true },
{ "name": "Stitching", "order": 2, "isPaid": true },
{ "name": "Packing", "order": 3, "isPaid": false }
],
"articleAttributes": [
{ "name": "color", "label": "Color", "type": "string", "isRequired": true },
{ "name": "size", "label": "Size", "type": "number", "isRequired": true }
],
"sizeRange": { "min": 6, "max": 12 },
"customLedgerTypes": ["advance", "bonus"],
"pagination": { "defaultPageSize": 50, "maxPageSize": 200 }
}
Error Responses
| Status | Meaning |
|---|---|
| 400 | Invalid config shape |
| 404 | Tenant not found |
Global Config Endpoints (/api/v1/global-config)
All routes require super-admin role. Manages platform-level configuration (not per-tenant).
| Method | Path | Description |
|---|---|---|
| GET | /global-config/cors |
List dynamic CORS origins |
| POST | /global-config/cors |
Add a CORS origin { origin: string } |
| DELETE | /global-config/cors/:origin |
Remove a CORS origin (URL-encoded) |
Dynamic CORS origins are polled by the API every 30 seconds. New origins take effect within 60 seconds without a restart.
Plan Endpoints (/api/v1/plan)
| Method | Path | Auth | Role | Description |
|---|---|---|---|---|
| GET | /plan |
Yes | any | List all plans |
| GET | /plan/:slug |
Yes | any | Get a single plan by slug |
| POST | /plan |
Yes | super-admin | Create a new plan |
| PATCH | /plan/:slug |
Yes | super-admin | Update a plan (slug is immutable) |
| DELETE | /plan/:slug |
Yes | super-admin | Delete a plan (blocked if tenants are on it) |
Plan Object
{
"_id": "...",
"name": "Professional",
"slug": "professional",
"description": "Mid-tier plan for growing factories",
"limits": {
"maxUsers": 10,
"maxWorkers": 100,
"maxJobSteps": 20,
"maxArticleAttributes": 20
},
"rateLimits": {
"maxRequests": 200,
"windowMs": 900000
},
"isActive": true
}
Error Responses
| Status | Meaning |
|---|---|
| 400 | Validation error |
| 404 | Plan not found |
| 409 | Plan slug already exists, or plan is in use by tenants (on delete) |
Worker Endpoints (/api/v1/worker)
All routes require admin or super-admin role. All queries are tenant-scoped.
| Method | Path | Description |
|---|---|---|
| GET | /worker |
List all workers |
| POST | /worker/create |
Create a worker |
| GET | /worker/:id |
Get a worker |
| PATCH | /worker/:id |
Update a worker |
| DELETE | /worker/:id |
Delete a worker |
| GET | /worker/initDefaultWorker |
Seed the default "None" worker for a tenant |
Worker skills are stored as a
skills: [String]array on the Worker document. There are no hardcoded role flags (upperMan,bottomMan, etc.) — skill filtering is done by querying theskillsarray.
Error Responses
| Status | Meaning |
|---|---|
| 400 | Validation error |
| 404 | Worker not found |
Article Endpoints (/api/v1/article)
All routes require admin or super-admin role. All queries are tenant-scoped.
| Method | Path | Description |
|---|---|---|
| GET | /article |
List all articles |
| POST | /article/create |
Create an article |
| GET | /article/getArticleList |
Lightweight list for dropdowns |
| POST | /article/:id |
Get a single article |
| PATCH | /article/:id |
Update an article |
| DELETE | /article/:id |
Delete an article |
Error Responses
| Status | Meaning |
|---|---|
| 400 | Validation error |
| 404 | Article not found |
Job Endpoints (/api/v1/job)
All routes require admin or super-admin role. All queries are tenant-scoped.
| Method | Path | Description |
|---|---|---|
| POST | /job |
List all jobs (POST to support body filters) |
| POST | /job/create |
Create a new job |
| GET | /job/:id |
Get a single job |
| PATCH | /job/:id |
Update job step status |
| DELETE | /job/:id |
Delete a job |
Error Responses
| Status | Meaning |
|---|---|
| 400 | Validation error |
| 404 | Job not found |
Ledger Endpoints (/api/v1/ledger)
All routes require admin or super-admin role. All queries are tenant-scoped.
| Method | Path | Description |
|---|---|---|
| POST | /ledger/worker/:workerId |
Get ledger entries for a worker (date filters in body) |
| POST | /ledger/summary/:workerId |
Get aggregated wage summary for a worker |
| POST | /ledger/adjustment |
Create a manual wage adjustment entry |
Error Responses
| Status | Meaning |
|---|---|
| 400 | Invalid date range or missing workerId |
| 404 | Worker not found |
Report Endpoints (/api/v1/report)
All routes require admin or super-admin role. All queries are tenant-scoped.
| Method | Path | Description |
|---|---|---|
| POST | /report/getTaskReportByGroup |
Grouped production report |
| POST | /report/getReportByArticleId/:id |
Report filtered by article |
Transaction Endpoints (/api/v1/transaction)
All routes require admin or super-admin role. All queries are tenant-scoped.
| Method | Path | Description |
|---|---|---|
| POST | /transaction |
List all transactions |
| POST | /transaction/create |
Create a transaction |
| POST | /transaction/makePayment |
Process a payment |
| GET | /transaction/:id |
Get a single transaction |
| PATCH | /transaction/:id |
Update a transaction |
| DELETE | /transaction/:id |
Delete a transaction |
Error Responses
| Status | Meaning |
|---|---|
| 400 | Validation error |
| 404 | Transaction not found |
Master Data Endpoints (/api/v1/master-data-values)
All routes require admin or super-admin. Tenant-scoped. Stores allowed values for each dynamic article attribute defined in TenantConfig.articleAttributes.
| Method | Path | Description |
|---|---|---|
| GET | /master-data-values/:tenantId |
Fetch all attribute value sets for a tenant |
| POST | /master-data-values |
Upsert value list for a specific attribute |
There are no separate endpoints per attribute type (no
/color,/material,/sole,/box). All attribute values are managed through a singleMasterDataValuescollection keyed byattributeName.