Skip to content
4booserPublic

About

Psychodiagnostic platform for the psychology department of an outpatient clinic

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Polypsy

Psychodiagnostics platform for a clinical institution. Specialists assign validated questionnaires, patients complete them on a phone or in a browser, and the system scores them, raises risk alerts, and keeps the clinical record around the results: appointments, conclusions, safety plans, referrals and follow-up.

Production runs at https://polypsy.ink. The repository keeps its original name, Quizzy.

Part Stack Path
API Bun, Hono, Drizzle ORM, PostgreSQL 16 with row-level security apps/api
Web React 19, Vite, Tailwind v4, TanStack Query apps/web
Mobile app Expo SDK 57, React Native, expo-router apps/mobile
Shared core Types, zod schemas, scoring engine, dictionaries packages/shared

The web app has three faces, chosen by role:

  • the clinical console for staff;
  • the patient cabinet;
  • the tech panel (/ops) for superadmins.

The mobile app serves patients and staff on rounds.

Quick start

Requirements: Bun, PostgreSQL 16. Recording transcription also needs ffmpeg and whisper.cpp; everything else works without them.

createdb quizzy
cp apps/api/.env.example apps/api/.env   # set DATABASE_URL

bun install
bun run db:migrate
bun run db:seed        # demo institution, methods, patients and history; prints the demo sign-ins
bun run api            # API on :3001
bun run web            # web on :5199
bun run mobile         # Expo on :8081, separate terminal
  • Outside production the secrets have development defaults. Production refuses to start without them (see apps/api/src/env.ts).
  • To point the mobile app at production, run bun run --cwd apps/mobile start:prod, or set EXPO_PUBLIC_API_URL.
  • To set up a clean instance for a new institution, use bun run install:instance instead of the seed. It migrates the database, creates the institution and installs the method catalog.

Demo sign-ins

  • The seed creates demo staff and patient accounts and prints their sign-ins when it finishes. They exist only on local and test databases; the e2e stand uses the same set (e2e/helpers.ts).
  • A real instance gets its first superadmin from install:instance: the email comes from ADMIN_EMAIL, and a random password is printed once. Further superadmins are added with apps/api/src/addSuperadmin.ts.

Languages

  • The interface is available in Ukrainian, Russian and English.
  • Method content (questions, options, band labels) is stored in Ukrainian and Russian.
  • The API answers in the language from ?lang= or Accept-Language, so clients receive plain strings.
  • Server messages, errors and notes are stored as codes and rendered in the reader's language.
  • Tests fail on hard-coded strings in the server, web or mobile code.

Roles and permissions

  • Patient: takes assigned methods and sees own results, appointments, messages and safety plan.
  • Staff:
    • positions form a ladder: specialist → head of department → chief;
    • on top of their role, staff get fine-grained permissions (patients.read, conclusions.sign, users.manage, ops.read, …) and personal exceptions;
    • what a staff member sees is limited to their groups and departments.
  • Superadmin runs the institution and the tech panel.

Rules that hold everywhere:

  • Any action on another account follows one rule, whether it comes through the web, the console or a bulk action: you need the permission, the account can't be your own, it must be strictly below your position, and only a superadmin may touch a superadmin. This covers role changes, password reset, disabling or deleting an account, sessions, the second factor, signing in as another user, and roles.
  • Reading a patient's clinical data goes through one check: conclusions, results, printouts and one-time print links all use it.
  • The database enforces the same boundaries with row-level security under a dedicated application role. Tests run the patient and staff flows under that role.
  • TOTP is the second factor. A policy makes it mandatory for superadmins and tech-panel users; other staff can turn it on themselves.
  • Patients join through an invitation link. Self-registration without an invitation (OPEN_REGISTRATION) is off in production unless set to 1, and on by default in development; it always creates a patient, never staff. With it off, the refusal is the same whether or not the phone or email is already known.
  • Pseudonymous patient accounts store no name.

What it does

Assessment

  • A method constructor with a JSON view for large instruments. Edits create versions, so every result is scored by the version the patient actually saw.
  • Assignment: individually, by patient group, by battery with step order, by schedule, or by cascade (a result band assigns follow-up methods).
  • Lists act on selected rows, with "select all" across pages. In the catalog, selected methods can be assigned, moved to a group and folder, retired or restored. Access, battery assignments and invitations can be revoked in bulk.
  • Patients take methods on the phone (with an offline queue) or in the browser. A clinician can fill in a method on the patient's behalf.
  • Critical answers raise risk alerts on autosave, not at the end. Alerts become cases with an owner and a history.

Clinical work

  • Reception and the day screen, appointment booking with Google Calendar / Meet links, and session recordings with local transcription.
  • Conclusions with revisions and signatures, visit notes, safety plans, referrals, and dispensary follow-up with due dates.
  • Messages and mailings to patients, with push notifications.
  • Printouts: a result sheet, answer key sheet, blank form, visit certificate, the full outpatient chart and a case extract. The mobile app prints through a one-time link.

Analytics

  • Patient dynamics with the reliable change index (RCI) and percentiles.
  • Psychometrics: Cronbach's alpha and item-total correlations, answer-quality flags (too fast, straight-lining), and local norms.
  • Cohorts, statistical models, a statistics builder, and export to CSV and SPSS with stable subject codes.

Tech panel (/ops)

  • Requests, errors, logs, traces, database and slow queries, jobs, sessions, the audit log, integrity of the audit chain, and read-only SQL.
  • Releases, feature flags, keys, mobile devices and push, recordings, client errors, web vitals, suspicious activity, grants, "who viewed" and second-factor status.
  • Alerts go to Telegram and email.

Scoring engine

Built both for the instruments in the manuals of the Research Center for Humanitarian Problems of the Armed Forces of Ukraine and for international screeners. The engine lives in packages/shared/src/scoring.ts; the server and the clients score the same way.

Mechanism Example
Key by item numbers, one item feeding several scales SR-45, MLO, Mini-mult
Ratio scales (share of keyed answers) SR-45 Sr = N/35
Scale-on-scale corrections Mini-mult K-correction Hs + 0.5K
T-scores by sex and age, sten tables Mini-mult, MLO
Validity scales that gate interpretation lie and validity scales
Reversed grading, clinical recommendation per band SR-45
Clinician-administered methods SAD PERSONS, HAM-D-17, HAM-A, CGI-S, CGI-I, BPRS-18
Minimum answered share per scale; items hidden by display logic excluded ASSIST, PC-PTSD-5

Scoring runs in a fixed order:

  1. raw scores for every scale;
  2. corrections, applied only after all raw scores exist;
  3. normalization;
  4. picking the band;
  5. the validity check.

When several norms or sten rows fit a person, the most specific one wins: sex first, then age, then the narrower age range.

validateSurvey blocks publishing a method that cannot be scored correctly: items out of range, contradictory keys, overlapping bands, T-scores without norms, and so on. Drafts can still be saved.

Methods

Shared catalog, 73 methods in ten areas. The catalog is code (apps/api/src/instruments/catalog.ts; areas in catalogDomains.ts), and the catalog screen filters by area.

Area Methods
Mood, anxiety, distress (20) PHQ-9, PHQ-8, PHQ-4, PHQ-2, PHQ-15, GAD-7, GAD-2, PSS-10, PSS-4, CESD-R, SRQ-20, GDS-15, DASS-42, DASS-21, K10, K6, CORE-10, AAQ-II, HAM-D-17, HAM-A
Trauma (8) PCL-5, PC-PTSD-5, ITQ, CES, MIOS, TGI-SR+, CTSR, LEC-5
Suicide risk (6) C-SSRS screener, ASQ, SBQ-R, SIDAS, SR-45, SAD PERSONS
Sleep (4) SCI, SCI-02, RIS, FAS
Addictions (7) AUDIT, AUDIT-C, CAGE, ASSIST, CUDIT-R, CIWA-Ar, COWS
Resources and quality of life (12) WHO-5, SWLS, Flourishing Scale, SPANE, MLQ, MSPSS, OSSS-3, RSES, UCLA-3, Brief COPE, SCS-SF, MAAS
Cognitive functions (3) ASRS-v1.1 (6 items), 4AT, ADEXI
Personality (4) IPIP-50 (Big Five), TIPI, MLO "Adaptivnist-200", Mini-mult
Staff (4) CBI, BAT-23, BAT-12, ProQOL 5
Other (5) PQ-16, AQ-10, CGI-S, CGI-I, BPRS-18
  • Install and update. Every deploy installs the catalog and updates it by content fingerprint.
  • Local edits survive. If an institution edited a method locally, its edit is kept: a locally published version blocks the content update, and local settings (visibility, instructions, time limit, area and the like) survive a regular update while the rest of the method is refreshed. The catalog-status and catalog-force maintenance actions show the situation and let you override it.
  • Who sees the catalog. Catalog methods live in the survey group «Каталог методик», a library shared by all staff: every clinician finds, reads, assigns and administers them and puts them into batteries without being an administrator of the group. Their results are shown only for patients who are already in the clinician's scope (department, own appointments, the department's own groups); the catalog never widens that scope, so taking a catalog method does not make a person visible to everyone. Editing catalog methods (builder, versions, settings, retiring) stays with superadmins and explicit administrators of the group. The same rule is enforced by row-level security.
  • Clinical passport and stepped screening. Every catalog method has a role (full instrument, short screening form, clinician-rated), item count, typical minutes, what it measures and its cut-offs (apps/api/src/instruments/catalogPassport.ts). The catalog screen groups methods by area with full instruments first and short forms nested under them (GET /api/surveys?sort=catalog). Ten screening steps with cited thresholds (PHQ-2 ≥ 3 → PHQ-9, GAD-2 ≥ 3 → GAD-7, AUDIT-C ≥ 4 → AUDIT, PC-PTSD-5 ≥ 4 → PCL-5, …) are installed once as suggest_survey rules: a positive short form suggests the full instrument on the result, in the patient's plan and in the assign drawer, and one click assigns it.
  • Battery templates. There are nine: initial screening, trauma in depth, suicide risk in depth, addictions, sleep and fatigue, resources and recovery, staff burnout and stress, the psychiatrist's initial examination (C-SSRS, HAM-D-17, HAM-A, CGI-S, all filled in by the doctor), and an in-depth psychological examination (SR-45, MLO, Mini-mult). Each is created once and then belongs to the institution: later deploys do not overwrite edits or bring back a deleted template.

The institution's methods, 4: SR-45, SAD PERSONS, Mini-mult and MLO "Adaptivnist-200" from the manuals of the Research Center for Humanitarian Problems of the Armed Forces of Ukraine. Since 28 October 2026 they are part of the shared catalog by the product owner's decision: the institution uses them and confirms its right to; there is no licence statement from the rights holders, and the dossiers, the staff notes and the install log say so. SR-45, MLO and Mini-mult are given to a patient only by assignment, as part of an examination; SAD PERSONS is filled in by a specialist and is never offered to patients. The seed keeps its own copies under the same catalog keys, so a stand with both the seed and the catalog shows each method once.

The methods have not been clinically validated yet. docs/instruments/ holds the validation package for a clinical psychologist:

  • one sheet per method in Ukrainian and Russian;
  • generated from the live engine, with "answers → result" examples at every band boundary;
  • a test fails whenever a sheet and the engine disagree. To regenerate the sheets, run bun run --cwd apps/api docs:instruments.

Licensing and sources are tracked in the dossiers under docs/instruments/dossiers/ and in docs/INSTRUMENTS.md.

Data and security

  • One server, one institution. Each institution gets its own database; there are no tenant columns.
  • Row-level security. The app connects as quizzy_app, and policies limit every table.
  • Audit log.
    • Append-only, and it records reads of patient data, not just changes.
    • Each row is chained by hash. The chain is verified on a schedule, and from the tech panel or bun run --cwd apps/api audit:verify.
  • Field encryption. Names, phone numbers, free-text answers and clinical texts are encrypted with ENCRYPTION_KEY (v1:<base64 32 bytes>[,v2:…]; the first key is the active one). Phone numbers can be searched through a blind index with its own secret.
  • Separate secrets. Session signing, the phone index, export subject codes and field encryption each have their own secret, so rotating one does not break the others.
  • Concurrency. State changes are atomic: conclusions, referrals, mailings and safety plans check the revision the user saw and answer 409 instead of overwriting someone else's edit.

The architecture, invariants and threat model are in docs/ARCHITECTURE.md.

Tests

bun run typecheck
bun run lint
bun run test                               # all unit and API tests (API tests need DATABASE_URL)
bun run --cwd apps/api test:app-role       # patient and staff flows under the application role
bun run e2e                                # Playwright on its own stand: DB quizzy_e2e, ports 3199/4199
  • API tests recreate <database>_test on every run, so use your own database when running in parallel.
  • A test may take up to 30 s (--timeout 30000 in the scripts and in CI). Where speed matters, dedicated tests check it with explicit thresholds (dynamicsScale, rlsScale).
  • CI checks every pull-request commit once and cancels the run of a superseded commit; pushes are checked on main only, and release tags by the deploy workflow.
  • CI runs test files in a different order than macOS does. Tests use unique data and never rely on "only my rows in the table".
  • migrationJournal.test.ts walks the history since the last release and fails if a migration was added before one that already existed.

Deploy and operations

Deploys go through GitHub Actions. A pushed v* tag starts .github/workflows/deploy.yml:

  1. CI runs typecheck, lint, the API, app-role and client tests, e2e and the Docker build.
  2. Images are built and pushed to GHCR.
  3. The compose file and scripts are uploaded to the server and validated.
  4. A database snapshot is taken.
  5. The new image loads the server's environment as a pre-check. If it fails, the old version keeps running.
  6. compose up starts provision. It runs the migrations, checks every migration by hash (a skipped one stops the deploy), and grants the application role its rights.
  7. The shared catalog is installed, and the deploy confirms that the running version, commit and site names match the release.
  8. Health checks run inside the server and from outside over HTTPS.

What search engines and AI assistants see:

  • The root page carries the landing text in the HTML itself, for clients that don't run scripts; browsers hide it and render the app as usual. Every other path gets app.html, the same shell without that text and with noindex (apps/web/seoHtml.ts, apps/web/Caddyfile).
  • /robots.txt allows everything except /api/. /llms.txt describes the product for language models.
  • At container start apps/web/entry.sh fills in the site address (Open Graph, schema.org, the Sitemap line) and writes /sitemap.xml, but only when SITE_ADDRESS is a domain.

.github/workflows/maintenance.yml runs one-off actions on the server:

  • catalog install, status and force;
  • demo data fill and purge;
  • the RLS check;
  • audit chain verification;
  • leftovers;
  • API and web logs;
  • backup status and a restore drill (also runs monthly);
  • slot resync;
  • drafts of already submitted attempts (#197): a dry run that lists them, and an apply step that closes their duplicate alerts with a journal row and removes them.

Backups:

  • A nightly pg_dump, encrypted with gpg and verified by reading it back.
  • Kept as 7 daily, 4 weekly and 12 monthly copies (scripts/backup.sh). The snapshot taken before each deploy goes to its own predeploy/ folder (last 10 kept) and never pushes out the scheduled copies.
  • A weekly timer and the monthly workflow restore the newest copy into a throwaway database and compare it with production (scripts/verify-backup.sh).
  • Each copy is also uploaded to an S3-compatible bucket at another provider when OFFSITE_URL is set (scripts/offsite.sh); the offsite-status action checks that every local copy exists there.

Server setup, TLS, Google sign-in and incident handling are covered in docs/DEPLOY.md and docs/RUNBOOK.md.

Repository map

apps/api/src/routes      HTTP routes, one file per area
apps/api/src/lib         domain logic: access rules, scoring glue, scheduler, notifications
apps/api/src/db          schema, RLS context, migration runner
apps/api/drizzle         SQL migrations and their journal
apps/api/src/instruments method definitions (catalog and seed)
apps/web/src/pages       console, patient cabinet and /ops screens
apps/mobile/app          expo-router screens: patient tabs, rounds, surveys, analytics
packages/shared/src      types, schemas, scoring, dictionaries (uiStrings, errorStrings, serverStrings)
scripts                  backup, restore, restore check, instance upgrade, VPS setup
e2e                      Playwright scenarios and API snapshots
docs                     architecture, deploy, runbook, roadmap, instruments

Documentation

Most documents are in Russian; the validation package is in Ukrainian and Russian.

File Contents
docs/ARCHITECTURE.md data map, invariants, threat model
docs/DEPLOY.md VPS installation, domain and TLS, deploys, backups, Google sign-in
docs/RUNBOOK.md day-to-day operations and incident handling
docs/instruments/ validation package for the clinical psychologist
docs/INSTRUMENTS.md origin and legal status of the methods (older; the validation package is current)
docs/OMR.md feasibility of reading paper forms from a photo
docs/ROADMAP.md, docs/REWRITE-PLAN.md, docs/REDESIGN.md plans and design migration notes (historical, not kept fully up to date)

Project status

Stage: pilot, late beta. Production runs v2.2.0 since 27 October 2026. Version 2.0 brought the console, the patient cabinet and the mobile app to the new «Виразна» design; 2.1 added the shared catalog by area with battery templates; 2.2 reworked the console wording for clinicians.

  • Done: clinical console, patient cabinet, mobile app, tech panel, 73 methods in the shared catalog (since 28 October 2026 including 4 of the institution's own and 5 clinician-rated scales), CI with app-role and browser tests, automated deploys with pre-checks, encrypted and verified backups, audit chain, second factor, three interface languages.
  • Hardening: four external code reviews (about 90 findings, including privilege escalation, races and data leaks) are fixed and covered by tests. Every pull request also gets an automated code review.
  • Not done: clinical validation of the methods by a psychologist (the package is ready and waiting), off-site backups configured on the server, SSH hardening on the host, mobile store publication, real users and a second institution.
  • Scale: one VPS (4 cores, 8 GB) serves one institution at near-zero load; capacity is not the constraint, process is.

Known limitations

  • The methods have not been clinically validated; the validation package is waiting for a psychologist's review.
  • Several instruments are copyrighted. Check their licensing in the dossiers before clinical use.
  • The mobile app is not published to the App Store or Google Play; builds are run locally through Expo.
  • The off-site backup copy (scripts/offsite.sh, any S3-compatible storage) is built but not yet configured on the production server.
  • One server serves one institution. More institutions mean more instances (scripts/upgrade-instances.sh).

About

Psychodiagnostic platform for the psychology department of an outpatient clinic

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages