The pharma plant analogy

Think of ProBeya’s data model like a manufacturing site:

Organization          = Your company (UCB, Pfizer, Acme Pharma)
  └── Workspace       = A site or department (Site Braine, Quality Dept)
       └── Project    = A specific initiative (Packaging Line 3 Transformation)
            └── Board = The daily management board (SQCDP Board)
                 ├── Group    = A category row (Safety, Quality, Cost, Delivery, People)
                 │    └── Item    = A single record (near-miss report, action, observation)
                 │         └── Value  = Cell data for that item (status: "open", priority: "high")
                 └── Column   = A field definition (Status, Priority, Due Date, Owner)

Every API call operates within this hierarchy. You always need an organizationId (resolved from your subdomain), and then navigate down: workspace, project, board, item.

Data model reference

EntityWhat it representsID formatKey fields
OrganizationYour company tenantcuid2name, slug (subdomain), plan
WorkspaceDepartment or sitecuid2name, slug, level, parentId
ProjectInitiative or value streamcuid2name, workspaceId
BoardThe working surfacecuid2name, projectId
GroupCategory row on the boardcuid2name, color, sortOrder, boardId
ItemA task, issue, or recordcuid2name, groupId, boardId, assigneeId
ColumnField definitioncuid2name, type, boardId, config
ValueCell data (EAV pattern)cuid2itemId, columnId, value (JSONB)

All IDs are cuid2 strings — collision-resistant, URL-safe, and generated client-side when needed. No auto-incrementing integers.

Custom fields: the EAV pattern

ProBeya uses an Entity-Attribute-Value pattern for maximum flexibility. Instead of fixed columns in a database table, each board defines its own Columns (field definitions), and each Item stores its data as Values — one Value per item per column.

Column: { id: "col_1", name: "Status", type: "status", boardId: "brd_1" }
Column: { id: "col_2", name: "Priority", type: "label", boardId: "brd_1" }
Column: { id: "col_3", name: "Batch ID", type: "text", boardId: "brd_1" }

Item: { id: "item_1", name: "Yield deviation on batch BX-2026-0412" }
  Value: { itemId: "item_1", columnId: "col_1", value: {"label": "open", "color": "#ef4444"} }
  Value: { itemId: "item_1", columnId: "col_2", value: {"label": "high", "color": "#f97316"} }
  Value: { itemId: "item_1", columnId: "col_3", value: "BX-2026-0412" }

This means any board can have any combination of 25+ field types without schema migrations.

Supported column types

TypeStored asExample use
textStringBatch ID, lot number, description
numberNumberOEE percentage, cycle time, temperature
statusJSON (label + color)Open, In Progress, Done, Verified
labelJSON (label + color)High / Medium / Low priority
personUser IDOwner, reviewer, approver
dateISO 8601 stringDue date, target completion
checkboxBooleanCompleted, approved, verified
ratingNumber (1-5)Severity score, impact rating
formulaComputedAuto-calculated from other columns
dependencyItem ID arrayPredecessor/successor relationships
fileS3 object keyAttachments, evidence photos
linkURL stringExternal references

See the Custom Fields developer guide for the complete list and JSON schemas for each type.

Multi-tenancy

ProBeya is a shared-database, shared-schema multi-tenant system. Every tenant gets a unique subdomain:

acme.probeya.com       → organizationId: "clx_org_acme"
ucb-pharma.probeya.com → organizationId: "clx_org_ucb"

How isolation works:

  1. The subdomain is resolved in Next.js middleware to an organizationId
  2. The organizationId is injected into the tRPC context server-side
  3. Every database query runs inside a PostgreSQL transaction with SET LOCAL app.current_org_id
  4. Row-level security (RLS) policies ensure that even buggy code cannot leak cross-tenant data
// This is what happens inside every orgProcedure call:
await db.transaction(async (tx) => {
  await tx.execute(
    sql`SELECT set_config('app.current_org_id', ${orgId}, true)`
  );
  // All subsequent queries in this transaction are RLS-filtered
});

Never trust a client-supplied organizationId. The tenant identity always comes from the subdomain, resolved server-side. API keys are scoped to a single organization at creation time.

SQCDP: the five pillars

SQCDP stands for Safety, Quality, Cost, Delivery, and People. It is the standard visual management framework used in lean manufacturing and pharma operations. In ProBeya, each pillar maps to a Group on a board:

PillarColorWhat gets tracked
S — SafetyRedNear-misses, incidents, safety observations, PPE compliance
Q — QualityBlueDeviations, CAPA, right-first-time, batch rejections
C — CostGreenOEE, waste, scrap rate, energy consumption
D — DeliveryOrangeOn-time delivery, cycle time, schedule adherence
P — PeoplePurpleTraining completion, absenteeism, mood/engagement

KPIs are categorized by SQCDP pillar, so you can query them by category:

# Get all Quality KPIs that are breaching thresholds
curl https://acme.probeya.com/api/v1/kpis/alerts?boardId=clx_board_1&category=Q \
  -H "Authorization: Bearer $PROBEYA_KEY"

Action management and escalation

Actions follow a lifecycle with tier-based escalation:

Created → Assigned → In Progress → Done → Verified
                         |
                    (overdue or blocked?)
                         |
              T1 (Team) → T2 (Department) → T3 (Site)

When an action is escalated, it moves from the team board to the department or site board, increasing visibility and accountability. Each escalation is logged with a reason and timestamp for audit trail.

Roles and permissions

RoleCan doCannot do
OwnerEverything—
AdminManage members, settings, webhooks, API keysDelete the organization
MemberCreate/edit projects, boards, items, actionsManage settings, API keys
GuestView specific boards they are invited toCreate or edit anything

API keys inherit the permissions of the user who created them, scoped to the organization.