Data Models — Foot Factory
All data lives in MongoDB Atlas. Every collection is tenant-scoped via a tenantId field.
Entity Relationship Overview
erDiagram
TENANT ||--o{ TENANT_CONFIG : "has one"
TENANT ||--o{ USER : "has many"
TENANT ||--o{ WORKER : "has many"
TENANT ||--o{ ARTICLE : "has many"
TENANT ||--o{ JOB : "has many"
TENANT ||--o{ CUSTOMER : "has many"
TENANT ||--o{ MASTER_DATA_VALUES : "has many"
JOB ||--o{ JOB_ITEM : "contains"
JOB ||--o{ JOB_STEP : "has steps"
JOB_ITEM }o--|| ARTICLE : "references"
JOB_STEP }o--|| WORKER : "assigned to"
WORKER ||--o{ LEDGER_ENTRY : "has ledger"
WORKER ||--o{ TRANSACTION : "has transactions"
TENANT_CONFIG ||--o{ JOB_STEP_DEF : "defines steps"
TENANT_CONFIG ||--o{ ARTICLE_ATTR_DEF : "defines attributes"
Tenant
The MongoDB Tenant document is a lightweight reference — status, plan, name, and slug are authoritative in the Central Registry (Supabase).
{
_id: ObjectId, // matches Supabase tenant UUID
slug: String, // URL-safe identifier
settings: {
maxUsers: Number, // copied from plan at provisioning time
maxWorkers: Number,
},
contact: {
email: String,
phone: String,
},
domain: String, // e.g. "acme-corp.foot-factory.dotevolve.net"
domainStatus: String, // "active" | "pending" | "failed"
owner: ObjectId, // ref: User
createdAt: Date,
}
User (Profile Document)
Passwords are managed by Supabase Auth. The MongoDB User document stores only profile data.
{
_id: ObjectId,
supabaseId: String, // unique — links to auth.users.id
name: String,
username: String,
email: String,
role: String, // "admin" | "user" | "super-admin" (legacy field)
tenantId: ObjectId, // ref: Tenant
avatarUrl: String,
preferences: Mixed,
createdAt: Date,
}
TenantConfig
Defines the dynamic schema for a tenant — drives all UI rendering in the client app.
{
_id: ObjectId,
tenantId: ObjectId,
jobSteps: [{
name: String, // e.g. "Bottom", "Upper", "Lower", "Polish"
order: Number,
isPaid: Boolean,
}],
articleAttributes: [{
name: String, // e.g. "color", "sole", "material"
label: String,
type: String, // "string" | "number" | "boolean"
isRequired: Boolean,
}],
sizeRange: { min: Number, max: Number },
customLedgerTypes: [String],
pagination: { defaultPageSize: Number, maxPageSize: Number },
}
Article
{
_id: ObjectId,
tenantId: ObjectId,
articleId: String, // e.g. "ART-001"
articleName: String,
description: String,
stepRates: Map, // { "Bottom": 20, "Upper": 25, ... }
isDeleted: Boolean,
}
Worker
{
_id: ObjectId,
tenantId: ObjectId,
workerId: String, // e.g. "WRK-001"
firstName: String,
lastName: String,
phone: String,
email: String,
skills: [String], // e.g. ["Upper", "Bottom"]
address: { houseNo, area, city, state, pin, country },
amount: Number, // total earned (running total)
balance: Number, // unpaid balance
jobsPendingPayment: [{
jobId: ObjectId,
jobStepName: String,
completedDate: Date,
earnedAmount: Number,
}],
jobsPaid: [{ jobId, jobStepName, paidDate, paidAmount }],
}
Job
{
_id: ObjectId,
tenantId: ObjectId,
taskName: String,
jobStatus: String, // "In Progress" | "Completed"
items: [{
articleId: ObjectId,
articleName: String,
attributes: Map, // dynamic — driven by TenantConfig.articleAttributes
sizes: Map, // { "6": 10, "7": 15, ... }
totalQuantity: Number,
}],
steps: [{
stepName: String, // matches TenantConfig.jobSteps[].name
order: Number,
isPaid: Boolean,
status: String, // "Pending" | "In-Progress" | "Completed"
workerId: ObjectId,
}],
createdAt: Date,
}
LedgerEntry
{
_id: ObjectId,
tenantId: ObjectId,
workerId: ObjectId,
type: String, // "job_earning" | "payment" | "adjustment"
credit: Number,
debit: Number,
runningBalance: Number,
description: String,
referenceModel: String,
referenceId: ObjectId,
createdAt: Date,
}
Transaction
{
_id: ObjectId,
tenantId: ObjectId,
workerId: ObjectId,
amount: Number,
transactionType: String, // "Cash" | "Transfer"
createdAt: Date,
}