Metricis - Implementation Plan¶
Project: Metricis (https://metricis.app) Date: 2026-01-04
Overview¶
Metricis is a production-grade, mobile-first, offline-capable cognitive assessment platform for clinical research. Built on jsPsych, designed for single-institution deployment (like REDCap).
Key Decisions¶
| Decision | Choice |
|---|---|
| CAT Engine | TypeScript client-side (full offline) |
| Mobile | Capacitor native from start |
| Multi-tenancy | Single institution, multiple studies |
| Auth | Standalone (no SAML/OIDC) |
| REDCap Role | Platform = source document; REDCap = curated summaries |
| Consent | Built-in module (per-study choice) |
| Portal | Separate React app (/portal/) |
| ORM | SQLAlchemy 2.0 + Alembic |
| RBAC | Simple 3-tier (admin/researcher/coordinator) |
| Sites | Optional within studies |
Phase 1: Foundation (Critical Path)¶
Goal: PostgreSQL + Auth + Basic Portal
Database Schema¶
-- Users (standalone auth)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255),
name VARCHAR(255),
role VARCHAR(50) DEFAULT 'researcher', -- admin, researcher, coordinator
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Studies
CREATE TABLE studies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
status VARCHAR(20) DEFAULT 'draft',
config JSONB DEFAULT '{}',
supported_languages TEXT[] DEFAULT ARRAY['en'],
consent_mode VARCHAR(20) DEFAULT 'builtin',
redcap_enabled BOOLEAN DEFAULT FALSE,
redcap_field_mapping JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Sites (optional, for multi-center)
CREATE TABLE sites (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
study_id UUID NOT NULL REFERENCES studies(id),
code VARCHAR(50) NOT NULL,
name VARCHAR(255) NOT NULL,
timezone VARCHAR(50) DEFAULT 'UTC',
UNIQUE(study_id, code)
);
-- Participants
CREATE TABLE participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
study_id UUID NOT NULL REFERENCES studies(id),
site_id UUID REFERENCES sites(id),
participant_code VARCHAR(100) NOT NULL,
external_id VARCHAR(255),
preferred_language VARCHAR(10) DEFAULT 'en',
status VARCHAR(20) DEFAULT 'enrolled',
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(study_id, participant_code)
);
-- Sessions
CREATE TABLE sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
participant_id UUID NOT NULL REFERENCES participants(id),
status VARCHAR(20) DEFAULT 'started',
started_at TIMESTAMPTZ DEFAULT NOW(),
completed_at TIMESTAMPTZ,
device_info JSONB,
sync_status VARCHAR(20) DEFAULT 'pending'
);
-- Responses (trial data)
CREATE TABLE responses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES sessions(id),
module_code VARCHAR(100) NOT NULL,
trial_data JSONB NOT NULL,
summary_scores JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Files to Create¶
Server:
- server/app/db/__init__.py - Database connection
- server/app/db/models.py - SQLAlchemy models
- server/app/db/session.py - Async session
- server/alembic.ini - Migration config
- server/alembic/env.py - Alembic environment
- server/alembic/versions/001_initial.py - Initial migration
- server/app/services/auth.py - Auth service (argon2 + JWT)
- server/app/routers/auth.py - Login/register endpoints
- server/app/middleware/auth.py - JWT middleware
Portal (new):
- portal/package.json
- portal/vite.config.ts
- portal/src/main.tsx
- portal/src/App.tsx
- portal/src/pages/Login.tsx
- portal/src/pages/Dashboard.tsx
- portal/src/pages/Studies.tsx
- portal/src/pages/Participants.tsx
- portal/src/contexts/AuthContext.tsx
- portal/src/services/api.ts
Modify:
- server/app/config.py - Add DB URL, JWT settings
- server/requirements.txt - Add sqlalchemy, asyncpg, alembic, argon2-cffi, PyJWT
- package.json - Add portal workspace
Phase 2: Offline + Capacitor¶
Goal: Client-side persistence, native mobile wrapper
Files to Create¶
Client:
- client/src/services/offline/database.ts - Dexie schema
- client/src/services/offline/syncQueue.ts - Sync manager
- client/capacitor.config.ts - Capacitor config
- client/ios/ - iOS project (generated)
- client/android/ - Android project (generated)
Dexie Schema:
// client/src/services/offline/database.ts
import Dexie from 'dexie';
export class MetricisDB extends Dexie {
sessions!: Table<OfflineSession, string>;
responses!: Table<OfflineResponse, string>;
syncQueue!: Table<SyncQueueItem, string>;
batteries!: Table<CachedBattery, string>;
constructor() {
super('Metricis');
this.version(1).stores({
sessions: 'id, participantId, status, syncStatus',
responses: 'id, sessionId, moduleCode',
syncQueue: 'id, type, status, retryCount',
batteries: 'id, versionId'
});
}
}
Modify:
- client/package.json - Add dexie, @capacitor/core, @capacitor/ios, @capacitor/android
- client/src/services/api.ts - Offline-first wrapper
Phase 3: Consent Module¶
Goal: Built-in consent flow
Files to Create¶
client/src/modules/consent/ConsentModule.ts- jsPsych consent pluginclient/src/modules/consent/SignatureCapture.ts- Canvas signatureserver/app/routers/consent.py- Consent endpointsserver/alembic/versions/002_consent.py- consent_records table
Phase 4: Battery Builder¶
Goal: Admin UI for composing batteries from existing tasks
Files to Create¶
Portal:
- portal/src/pages/admin/BatteryBuilder.tsx - Drag-and-drop UI
- portal/src/pages/admin/ModuleRegistry.tsx - View all 33 tasks
- portal/src/components/battery/ModuleCard.tsx
- portal/src/components/battery/Timeline.tsx
Server:
- server/app/routers/batteries.py - Battery CRUD
- server/app/routers/modules.py - Module registry
- server/app/services/battery_compiler.py - Compile to client format
- server/alembic/versions/003_batteries.py
Client:
- client/src/orchestrator/BatteryOrchestrator.ts - Execute compiled battery
- client/src/orchestrator/ModuleLoader.ts - Load existing tasks dynamically
Phase 5: REDCap Integration¶
Goal: Configurable field mapping, platform as source document
Files to Modify/Create¶
server/app/services/redcap.py- Refactor for configurable mappingserver/app/services/redcap_sync.py- Background syncserver/app/routers/redcap.py- Config endpointsportal/src/pages/admin/REDCapConfig.tsx- Field mapping UIserver/alembic/versions/004_redcap_sync_log.py
Phase 6: TypeScript CAT Engine¶
Goal: Client-side IRT for offline adaptive testing
Files to Create¶
Client CAT Engine:
- client/src/cat/CATEngine.ts - Main engine
- client/src/cat/irt/TwoParameterLogistic.ts - 2PL model
- client/src/cat/irt/GradedResponseModel.ts - GRM for Likert
- client/src/cat/estimation/MaximumLikelihood.ts - MLE theta
- client/src/cat/selection/MaximumInformation.ts - Item selection
- client/src/cat/termination/StandardError.ts - Stopping rules
- client/src/cat/CATModule.ts - jsPsych integration
- client/src/cat/types.ts - Type definitions
Server:
- server/app/routers/item_banks.py - Item bank management
- server/alembic/versions/005_item_banks.py
Phase 7: Scheduling & Reminders¶
Goal: Visit windows, automated notifications
Files to Create¶
server/app/services/scheduler.py- Schedule managementserver/app/services/notifications.py- Email/SMS deliveryserver/app/workers/reminder_worker.py- Celery taskserver/app/routers/schedules.py- Schedule CRUDserver/celeryconfig.py- Celery configportal/src/pages/Schedules.tsx- Schedule UIclient/src/services/pushNotifications.ts- Capacitor pushserver/alembic/versions/006_schedules.py
Phase 8: Reports & Export¶
Goal: Complete researcher portal
Files to Create¶
portal/src/pages/Reports.tsx- Summary reportsportal/src/pages/ParticipantDetail.tsx- Individual viewportal/src/pages/Export.tsx- Data exportserver/app/services/reporting.py- Report generationserver/app/services/export.py- CSV/JSON exportserver/app/routers/reports.pyserver/app/routers/exports.py
Critical Path¶
Phase 1 (Foundation) ──┬──> Phase 2 (Offline) ───> Phase 6 (CAT)
│
├──> Phase 3 (Consent)
│
└──> Phase 4 (Battery) ──> Phase 5 (REDCap)
│
└──> Phase 7 (Scheduling) ──> Phase 8 (Reports)
MVP (Phases 1-3): Database, auth, portal, offline, consent Full Platform: All 8 phases
Existing Code Preservation¶
33 jsPsych Tasks (unchanged)¶
Tasks in /client/src/tasks/ remain unchanged. The original 25 Tier 1 tasks plus 8 Tier 2 deep phenotyping tasks (vigilance-cpt, operation-span, tower-task, source-memory, pattern-separation, theory-of-mind, semantic-fluency, sentence-comprehension). New BatteryOrchestrator wraps them:
// client/src/orchestrator/BatteryOrchestrator.ts
export class BatteryOrchestrator {
async executeBattery(battery: CompiledBattery): Promise<void> {
for (const module of battery.modules) {
const createTimeline = taskRegistry.get(module.code);
const timeline = createTimeline(this.jsPsych, module.config);
await this.jsPsych.run(timeline);
await this.offlineStore.saveResponse(this.sessionId, module.code, data);
}
}
}
Existing Types (extended)¶
// Add to client/src/types/index.ts
export interface CompiledBattery {
id: string;
version: string;
modules: CompiledModule[];
consentRequired: boolean;
}
Existing REDCap Service (refactored)¶
/server/app/services/redcap.py - Keep PyCap core, add configurable field mapping.
Dependencies to Add¶
Server (server/requirements.txt):
sqlalchemy[asyncio]>=2.0
asyncpg>=0.29
alembic>=1.13
argon2-cffi>=23.1
PyJWT>=2.8
celery>=5.3
redis>=5.0
Client (client/package.json):
{
"dependencies": {
"dexie": "^4.0",
"@capacitor/core": "^5.0",
"@capacitor/ios": "^5.0",
"@capacitor/android": "^5.0",
"@capacitor/push-notifications": "^5.0"
}
}
Portal (portal/package.json):