Admin, Compliance & Deployment

Part of: Pickatale Master Build Wiki | Version: v1.7 | Last Updated: 2026-04-18


24. Admin and Operational Tools

Source: (D) | Status: Confirmed as spec (must build)

Access

24.1 Search User

24.2 Impersonate User

24.3 Reset Password (Admin)

24.4 Grant / Revoke Entitlement

24.5 Move Child Between Accounts / Schools

24.6 Merge Duplicate Accounts

24.7 View Audit Logs

24.8 Resend Invite

24.9 Disable / Suspend Account

24.10 View Payment Status

24.11 View Learner Event History



44. Admin APIs

Source: (D) | Status: Confirmed as spec. All admin endpoints live at /api/admin on Account Center (port 3126). MUST BUILD.**

44.1 Admin Endpoints

All admin endpoints require uc_session with role='platform_admin'. All admin endpoints INSERT audit_log before executing.

GET    /api/admin/users                         List users (filterable by role, state, school_id)
GET    /api/admin/users/:id                     Get user detail
PATCH  /api/admin/users/:id                     Update user (state, role, school_id)
POST   /api/admin/users/:id/suspend             Suspend account
POST   /api/admin/users/:id/reactivate          Reactivate account
POST   /api/admin/users/:id/impersonate         Start impersonation session
DELETE /api/admin/impersonate                   End impersonation session

GET    /api/admin/schools                       List schools
GET    /api/admin/schools/:id                   Get school detail + member list
PATCH  /api/admin/schools/:id                   Update
GET    /api/admin/subscriptions                 List subscriptions (filterable by state, tier)
PATCH  /api/admin/subscriptions/:id            Update subscription state manually
POST   /api/admin/entitlement/grant            Grant entitlement to user or school
DELETE /api/admin/entitlement/grant/:id        Revoke entitlement grant

GET    /api/admin/students/:id                 Get student detail (any school)
POST   /api/admin/students/merge               Merge duplicate students (see Flow 30.10)
PATCH  /api/admin/students/:id/transfer        Transfer student to different school
POST   /api/admin/students/:id/reset-pin       Reset PIN for any student

GET    /api/admin/audit-log                    Query audit log (filters: actor_id, action, date range)
GET    /api/admin/jobs/:name/runs              View cron job run history
POST   /api/admin/bot/run-learner/:id          Manually trigger bot for one learner
POST   /api/admin/bot/run-nightly              Manually trigger full nightly run

44.2 Admin Permission Model

Role Can access /api/admin Can impersonate Can cross-tenant Can grant entitlement
platform_admin βœ… all endpoints βœ… any user βœ… βœ…
school_admin ❌ ❌ ❌ ❌
teacher ❌ ❌ ❌ ❌

school_admin manages their own school via regular /api/v1/ endpoints (not /api/admin/).

44.3 Impersonation Safeguards

POST /api/admin/users/:id/impersonate
Auth: uc_session role=platform_admin
Input: { "reason": "string (required, min 10 chars)" }

Backend:
  1. Verify requestor is platform_admin
  2. Target user must NOT be another platform_admin (no admin→admin impersonation)
  3. INSERT audit_log (action='impersonation_started', actor_id=admin_id,
       target_id=user_id, metadata={reason, ip, user_agent})
  4. INSERT sessions (user_id=target_id, role=target_role, impersonator_id=admin_id,
       expires_at=now()+1h, ip, user_agent)
  5. Set uc_session cookie with impersonated session

Active impersonation indicators:
  - All API responses include header: X-Impersonated-By: {admin_name}
  - Client must show orange "Impersonating {name}" banner at all times
  - All mutations during impersonation: audit_log includes impersonator_id in metadata

End impersonation:
DELETE /api/admin/impersonate
  1. Invalidate current session
  2. INSERT audit_log (action='impersonation_ended', metadata={duration_seconds})
  3. Restore admin's own session (stored in cookie as X-Admin-Session, httpOnly)

Constraint: Impersonation sessions expire in 1 hour. No extension. No impersonation of platform_admin by platform_admin.

44.4 Audit Log Schema (Final)

-- Already in Section 38.1 as CREATE TABLE audit_log
-- Action values (complete list):

Auth:            register, email_verified, login, logout, forgot_password, password_reset,
                 invite_sent, invite_accepted, account_locked, account_suspended, account_reactivated

Identity:        email_changed, password_changed

Session:         session_created, session_expired, session_invalidated

Roster:          create_class, archive_class, add_student, bulk_import, move_student,
                 transfer_student, archive_student, merge_students, reset_student_pin,
                 pin_revealed, print_login_cards, parent_claim_submitted, parent_claim_approved,
                 parent_claim_rejected

Entitlement:     entitlement_grant_created, entitlement_grant_revoked, subscription_expired,
                 entitlement_check_failed

Billing:         stripe_webhook, subscription_created, subscription_updated, payment_succeeded,
                 payment_failed, subscription_cancelled

Admin:           impersonation_started, impersonation_ended, admin_user_updated,
                 admin_subscription_updated, admin_bot_run_triggered

GDPR:            data_export_requested, data_export_ready, data_deletion_requested,
                 data_deletion_completed

Compliance:      coppa_consent_received, coppa_consent_revoked


45. Compliance Workflows

Source: (D) | Status: [V1-PRODUCTION]

45.1 Student Activation β€” School DPA Model [V1-PRODUCTION]

v1 Rule: Student accounts are activated under the school's DPA. The school is the data controller. No per-student consent gate on the platform. No age-based activation restriction implemented in code.

Age-related rules that DO exist in v1:

Legal responsibility: Schools manage parental notification externally (letters, privacy notices). Platform does not verify, store, or gate on parental consent status.

45.2 GDPR Data Export and Deletion

Trigger: Authenticated user (or platform_admin on behalf) submits deletion or export request.

Export flow:

POST /api/gdpr/export
Auth: uc_session (any role β€” exports own data; platform_admin can target any user_id)
Input: { "user_id": optional (admin only) }

Backend:
  1. INSERT gdpr_requests (user_id, type='export', state='pending')
  2. Queue async export job (runs within 24h)
  3. Job collects from: Account Center (users, sessions), Reader App (reading_sessions, quiz_attempts),
       Telemetry (events), Learner Bot (memories, vocab_gaps, reports), LRS (xAPI statements),
       Teacher Portal (student data if applicable)
  4. Package as JSON. Upload to secure temporary URL (24h expiry).
  5. UPDATE gdpr_requests.state='complete', download_url=...
  6. Send DATA_EXPORT_READY email
  7. INSERT audit_log (action='data_export_ready')

Deletion flow:

POST /api/gdpr/delete
Auth: uc_session OR platform_admin
Input: { "learner_id": "UUID (for child deletion)" OR user_id }

Backend:
  1. INSERT gdpr_requests (type='deletion', state='pending')
  2. Immediate: UPDATE students.state='archived'
  3. Async job (within 30 days per GDPR):
     a. Telemetry DB: DELETE events WHERE learner_id = target
     b. Learner Bot DB: DELETE memories, vocab_gaps, assessments, reports WHERE learner_id = target
     c. Reader App DB: DELETE reading_sessions, quiz_attempts, placement_results WHERE learner_id = target
     d. LRS DB: DELETE xAPI statements WHERE actor = learner_id
     e. Account Center: anonymise users row (name β†’ 'Deleted User', email β†’ uuid@deleted.invalid)
     f. audit_log: RETAIN (legal requirement β€” action log, no PII in metadata after deletion)
  4. UPDATE gdpr_requests.state='complete'
  5. INSERT audit_log (action='data_deletion_completed')

45.3 Data Retention

Data type Retention Notes
Telemetry events 90 days Cron deletes at 03:00 UTC
Raw touch data Session only Anonymised after session analysis (never stored raw)
Raw audio (fluency) 0 days Voice audio MUST NOT be stored. Audio may exist only in-memory during request processing and must be deleted immediately after transcription. No logging, caching, queues, or retries may contain raw audio.
Reading sessions 3 years Or until deletion request
Learner Bot memories 3 years Or until deletion request
LRS xAPI statements Indefinite Or until deletion request
Audit logs 7 years Retained for legal compliance (but PII removed on deletion)
nightly_reports 3 years Or until deletion request
emails sent 90 days email_log.status only
Session tokens 7 days Or until invalidated

45.4 Audit Trace for Compliance

Every data access or mutation touching child data must INSERT audit_log.

For SOC2 Type II: All of the following must be logged:

Audit log must be immutable: No UPDATE or DELETE on audit_log rows. Hard delete only on GDPR deletion β€” and only after anonymising the PII fields.

-- On GDPR deletion of a user:
UPDATE audit_log
  SET metadata = JSON_REMOVE(metadata, '$.email', '$.name', '$.ip')
WHERE actor_id = :user_id;
-- Do NOT delete the row. The action record must be retained.


46. Deployment Model

Source: (D) + code audit | Status: Confirmed for existing services; MUST BUILD where noted

46.1 Environments

Environment URL pattern Purpose
Production *.readingtester.com Live. Students use this.
Staging *.staging.readingtester.com Pre-release validation. Mirrors prod data schema, not data.
Local localhost:* Developer laptops.

Rule: No staging environment exists yet (MUST BUILD). Until it does: test on production with test accounts only. Never use real student data for testing.

Target pipeline per service:

Push to main β†’
  1. Install deps (pnpm install)
  2. Type check (tsc --noEmit)
  3. Lint (eslint)
  4. Unit tests (vitest or jest)
  5. Build (tsc || vite build)
  6. Docker build
  7. Push image to registry
  8. Run migrations (prisma migrate deploy || sequelize db:migrate)
  9. Deploy (docker pull + docker compose up -d on server via SSH)
 10. Smoke test (curl health endpoint, assert 200)

Current state: Manual SSH deploy. No CI pipeline. (MUST BUILD for v2)

46.3 Migrations

Rule: Every DB schema change must be a migration file. No hand-editing production tables.

Migration tools by service:

Migration procedure:

  1. Write migration file (numbered: 001_create_users.sql)
  2. Test on local
  3. Apply to staging (when exists)
  4. Apply to production during low-traffic window (UTC 02:00–04:00)
  5. Verify with health check before and after

Rollback: Every destructive migration must have a corresponding rollback file. Column drops require a two-phase migration: (1) stop writing, (2) drop β€” separated by at least one deploy.

46.4 Rollback Procedure

If production deploy fails:

  1. Re-deploy previous Docker image tag (keep last 3 tags in registry)
  2. If migration already ran: apply rollback SQL manually β€” never auto-rollback DB in production
  3. Alert: INSERT audit_log (action='rollback', metadata={service, version, reason})

46.5 Secrets Management

Rule: No secrets in source code or Docker images. Ever.

Secret type Storage
DB passwords .env in service root (gitignored β€” see internal ops docs for paths)
API keys (SendGrid, Stripe, OpenAI, Azure) Same .env
JWT secrets Same .env
Internal shared key Same .env β†’ INTERNAL_KEY=<stored in .env, not committed>
Stripe webhook secret Same .env β†’ STRIPE_WEBHOOK_SECRET=...

.env.example must exist for every service with all variable names and placeholder values.

Production .env files are on the server only. Never committed. Never in Docker image ENV instructions (use --env-file at runtime).

# Start pattern for all services:
docker compose --env-file .env up -d