Plan: REDCap System Structure Gap Analysis & Remediation¶
Overview¶
This plan identifies gaps between REDCap's conceptual system structure (as documented in docs/plans/redcap-system-structure.md) and Metricis's current implementation, then proposes targeted remediation.
Gap Analysis Summary¶
| REDCap Concept | Metricis Status | Gap Severity | Remediation |
|---|---|---|---|
| 8 User Roles | 6 Study Roles | Low | Mapping is appropriate for EDC context |
| Data Access Groups (DAGs) | Implemented | None | UserStudy.site_id filtering works |
| Per-Instrument Permissions | Not Implemented | Medium | Future enhancement |
| Record Locking | Not Implemented | Medium | Add for GCP compliance |
| E-Signatures | Not Implemented | High | Required for 21 CFR Part 11 |
| API/Integration Users | Partial | Medium | Add dedicated API key model |
| Audit Trail | Implemented | Low | AuditLog model exists |
| Project Lifecycle States | Partial | Low | Study has status but no enforcement |
Detailed Gap Analysis¶
Gap 1: E-Signatures (HIGH PRIORITY)¶
REDCap Feature:
- Record-level e-signatures for GCP/21 CFR Part 11 compliance
- Signature locks record from further editing
- Captures username, timestamp, reason
Current State:
- No e-signature support in Metricis
- Participants can be edited freely
Impact:
- Cannot use Metricis for FDA-regulated trials requiring 21 CFR Part 11 compliance
- No audit-ready proof of data integrity
Proposed Solution:
Add ParticipantSignature model and signature workflow:
# server/app/db/models.py
class ParticipantSignature(Base):
__tablename__ = "participant_signatures"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
participant_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("participants.id"))
signed_by_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
signature_type: Mapped[str] # "data_lock", "consent_verification", "query_resolution"
reason: Mapped[str]
signed_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
# Relationships
participant = relationship("Participant", back_populates="signatures")
signed_by = relationship("User")
Files to Modify:
server/app/db/models.py- Add ParticipantSignature modelserver/app/routers/participants.py- Add signature endpointsserver/app/schemas/participants.py- Add signature schemasportal/src/pages/ParticipantDetail.tsx- Add signature UI
Gap 2: Record Locking (MEDIUM PRIORITY)¶
REDCap Feature:
- Lock individual records to prevent edits
- Lock specific instruments within a record
- Locking is separate from e-signatures
Current State:
- No locking mechanism
- All records editable by anyone with EDIT_PARTICIPANTS permission
Impact:
- Data integrity risk in multi-user environments
- Cannot freeze records for audit
Proposed Solution:
Add is_locked and locked_at fields to Participant model:
# In Participant model
is_locked: Mapped[bool] = mapped_column(default=False)
locked_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
locked_by_id: Mapped[Optional[uuid.UUID]] = mapped_column(ForeignKey("users.id"), nullable=True)
Endpoint Changes:
PATCH /participants/{id}- Return 403 ifis_locked=TruePOST /participants/{id}/lock- Lock record (requires EDIT_PARTICIPANTS)POST /participants/{id}/unlock- Unlock record (requires MANAGE_USERS or owner)
Files to Modify:
server/app/db/models.py- Add lock fields to Participantserver/app/routers/participants.py- Add lock/unlock endpoints, enforce lock checkserver/alembic/versions/- Migration for new fieldsportal/src/pages/ParticipantDetail.tsx- Add lock/unlock UI
Gap 3: API/Integration Users (MEDIUM PRIORITY)¶
REDCap Feature:
- Dedicated API tokens per user
- Scoped permissions (read-only, write, etc.)
- Separate from UI authentication
Current State:
- JWT tokens used for both UI and API
- No dedicated API key model
- API access inherits user's full permissions
Impact:
- Cannot create service accounts for integrations
- No granular API permission scoping
- Security risk: long-lived JWT tokens
Proposed Solution:
Add ApiKey model for service integrations:
# server/app/db/models.py
class ApiKey(Base):
__tablename__ = "api_keys"
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"))
name: Mapped[str] # "REDCap Sync", "Data Export Script"
key_hash: Mapped[str] # bcrypt hash of the key
key_prefix: Mapped[str] # First 8 chars for identification
scopes: Mapped[list[str]] = mapped_column(ARRAY(String)) # ["read:participants", "write:sessions"]
study_id: Mapped[Optional[uuid.UUID]] = mapped_column(ForeignKey("studies.id"), nullable=True) # Scope to study
expires_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
last_used_at: Mapped[Optional[datetime]] = mapped_column(nullable=True)
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
Files to Modify:
server/app/db/models.py- Add ApiKey modelserver/app/routers/api_keys.py- New router for key managementserver/app/middleware/auth.py- Add API key authentication pathserver/app/services/access_control.py- Support scope-based permission checksportal/src/pages/Settings.tsx- API key management UI
Gap 4: Project Lifecycle States (LOW PRIORITY)¶
REDCap Feature:
- Development → Production → Archived states
- Production mode restricts schema changes
- Archived mode is read-only
Current State:
- Study has
statusfield (draft, active, completed, archived) - No behavioral enforcement of states
Impact:
- Schema changes possible on active studies
- No protection against accidental modifications
Proposed Solution: Enforce lifecycle constraints in routers:
# In study modification endpoints
if study.status == "archived":
raise HTTPException(403, "Cannot modify archived study")
if study.status == "active" and is_schema_change(data):
raise HTTPException(403, "Cannot modify schema on active study")
Files to Modify:
server/app/routers/studies.py- Add lifecycle checksserver/app/routers/batteries.py- Prevent battery changes on active studiesserver/app/routers/schedules.py- Prevent schedule changes on active studies
Gap 5: Per-Instrument Permissions (LOW PRIORITY - FUTURE)¶
REDCap Feature:
- Grant access to specific instruments per user
- Some users see only certain forms
- Form-level export restrictions
Current State:
- Permissions are study-level only
- All users with VIEW_STUDY see all batteries/forms
Impact:
- Cannot restrict access to sensitive instruments (e.g., psychiatric assessments)
- All-or-nothing data access within a study
Proposed Solution (Future):
Add UserStudyBattery junction table for granular permissions. This is a significant architectural change and should be deferred unless there's a specific regulatory requirement.
Implementation Priority¶
Phase 1: Critical Compliance (E-Signatures + Record Locking)¶
These are blockers for GCP/FDA-regulated trials.
- Add
ParticipantSignaturemodel - Add lock fields to Participant
- Create migration
- Add signature/lock endpoints
- Add portal UI for signatures and locking
Phase 2: Integration Improvements (API Keys)¶
Enables secure service-to-service integrations.
- Add
ApiKeymodel - Add API key authentication middleware
- Add key management endpoints
- Add portal UI for key management
Phase 3: Governance Hardening (Lifecycle Enforcement)¶
Prevents accidental changes to production studies.
- Add lifecycle state checks to study routers
- Add lifecycle state checks to battery/schedule routers
- Add warning UI in portal when editing active studies
Files to Modify (Summary)¶
New Files¶
server/app/routers/api_keys.py- API key managementportal/src/pages/ApiKeys.tsx- API key management UI
Modified Files¶
| File | Changes |
|---|---|
server/app/db/models.py |
Add ParticipantSignature, ApiKey, lock fields |
server/app/routers/participants.py |
Add signature/lock endpoints, enforce locking |
server/app/routers/studies.py |
Add lifecycle enforcement |
server/app/routers/batteries.py |
Add lifecycle enforcement |
server/app/middleware/auth.py |
Add API key auth path |
server/app/services/access_control.py |
Add scope-based permission checks |
portal/src/pages/ParticipantDetail.tsx |
Add signature/lock UI |
portal/src/pages/Settings.tsx |
Add API key management |
Migrations¶
server/alembic/versions/xxx_add_participant_signatures.pyserver/alembic/versions/xxx_add_participant_locking.pyserver/alembic/versions/xxx_add_api_keys.py
Verification Plan¶
E-Signatures¶
- Create participant in test study
- Add e-signature via API
- Verify signature appears in audit log
- Verify participant cannot be edited after signature (if lock-on-sign enabled)
Record Locking¶
- Lock participant via API
- Attempt to PATCH participant
- Verify 403 response with "Record is locked" message
- Unlock participant
- Verify PATCH succeeds
API Keys¶
- Create API key via portal
- Use key in Authorization header:
Authorization: Bearer mk_xxx - Verify scoped access works (can access scoped endpoints, denied others)
- Revoke key
- Verify key no longer works
Lifecycle Enforcement¶
- Create study in draft status
- Modify battery (should succeed)
- Change study to active
- Attempt to modify battery (should fail with 403)
- Archive study
- Attempt any modification (should fail with 403)
Not Addressed (Intentionally)¶
| REDCap Feature | Reason for Exclusion |
|---|---|
| Survey-Only Users | Metricis handles this via participant session tokens, not user accounts |
| Repeating Instruments | Not applicable - cognitive tasks are session-based, not form-based |
| Data Quality Rules | Out of scope - cognitive data is auto-scored, not manually entered |
| Randomization Module | Out of scope - handled by external randomization systems |
Risk Assessment¶
| Change | Risk Level | Mitigation |
|---|---|---|
| ParticipantSignature model | Low | Additive change, no existing data affected |
| Record locking | Low | Additive fields, default to unlocked |
| API Keys | Medium | New auth path, requires thorough security review |
| Lifecycle enforcement | Medium | Could break existing workflows, needs warning UI |