Skip to content

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


Goal: Built-in consent flow

Files to Create

  • client/src/modules/consent/ConsentModule.ts - jsPsych consent plugin
  • client/src/modules/consent/SignatureCapture.ts - Canvas signature
  • server/app/routers/consent.py - Consent endpoints
  • server/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 mapping
  • server/app/services/redcap_sync.py - Background sync
  • server/app/routers/redcap.py - Config endpoints
  • portal/src/pages/admin/REDCapConfig.tsx - Field mapping UI
  • server/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 management
  • server/app/services/notifications.py - Email/SMS delivery
  • server/app/workers/reminder_worker.py - Celery task
  • server/app/routers/schedules.py - Schedule CRUD
  • server/celeryconfig.py - Celery config
  • portal/src/pages/Schedules.tsx - Schedule UI
  • client/src/services/pushNotifications.ts - Capacitor push
  • server/alembic/versions/006_schedules.py

Phase 8: Reports & Export

Goal: Complete researcher portal

Files to Create

  • portal/src/pages/Reports.tsx - Summary reports
  • portal/src/pages/ParticipantDetail.tsx - Individual view
  • portal/src/pages/Export.tsx - Data export
  • server/app/services/reporting.py - Report generation
  • server/app/services/export.py - CSV/JSON export
  • server/app/routers/reports.py
  • server/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):

{
  "dependencies": {
    "react": "^18",
    "@tanstack/react-query": "^5.0",
    "@dnd-kit/core": "^6.0",
    "recharts": "^2.0"
  }
}