Hackorda Docs

Database Schema

Hackorda's data layer is PostgreSQL accessed through Drizzle ORM. The schema is the source of truth for a crowd-QA / bug-bounty platform: organizations run test cycles against their products, testers file issues, admins triage and settle payouts, and a small set of platform tables (API keys, notifications, audit, rate-limiting) keep it safe to operate.

A legacy LMS / quiz / hackathon-event data model also lives in the same database. It is off the current product path — present, migrated, but not where the active QA work happens. It's documented at the end for completeness.

How the schema is organized

The schema is split by domain under src/db/schema/<domain>.ts, each file owning its tables plus the enum constants those tables reference. Two barrels tie it together:

  • src/db/schema/index.ts — re-exports every domain file in dependency order (foundational tables first, leaf domains last).
  • src/db/schema.ts — a 12-line barrel that does nothing but export * from "./schema/index". It exists so drizzle.config.ts (schema: './src/db/schema.ts') and the ~108 callers that import … from "@/db/schema" never had to change when the single file was split into a folder. There is no monolithic schema file anymore — to add a table, drop it in the right domain file and re-export it from schema/index.ts.

Domain files:

FileMain tablesPurpose
_enums.tsroles, quizStatuses, selectionCriteria, moduleTypes, courseVisibilitiesCross-cutting const enums + their lookup/reference tables
institutions.tsinstitutionsKazakhstan education-institution catalog (~2400 seeded rows)
users.tsusers, userEducation, adminImpersonationLogUser profiles, education history, "view as user" audit trail
test-cycles.tsorganizations, products, productVersions, testCycles, cycleDocuments, testers, testRunsThe QA catalog spine + cycle membership + execution sessions
test-cases.tstestCases, testSuites, testSuiteCases, cycleTestCases, testExecutionsReusable structured test-case library + per-cycle plans + execution history
issues.tsissues, issueComments, attachments, issuePayouts, payoutBatches, issueVerifications, aiRunsFiled bugs, the payout ledger that settles them, verification attempts, AI provenance
integrations.tsorganizationIntegrationsExternal integration providers (Linear/Jira/GitHub/Anthropic) + per-org install state
notifications.tsnotificationsIn-app bell notifications, one row per (user, event)
audit.tsauditLogAppend-only trail of sensitive admin actions
infra.tsrequestIdempotency, rateLimitBucketsPer-request safety: idempotency keys + Postgres-backed rate limiting
api-keys.tsapiKeysScoped programmatic / agent access (Bearer hk_live_…)
learning.tscourses, modules, lessons, courseEnrollments, modulePermissions, moduleProgress, lessonProgressLegacy LMS (Techorda academy)
quiz.tsquestionSets, questionModules, questions, quizSessions, quizAnswersLegacy placement-quiz engine
events.tsevents, teams, teamInvitations, userTeams, plus submissions/scoringLegacy hackathon / event system

The role model

Roles are a normalized reference table, not a Postgres enum.

  • users.roleId is an integer FK → roles.id (roles lives in _enums.ts). roles is a seeded lookup table (id, name, description).

  • The valid values come from the ROLES const in _enums.ts:

    export const ROLES = {
      ADMIN: 1,
      STUDENT: 2,
      GUEST: 3,
      QA: 4,
      SUPER_ADMIN: 5,
    } as const;

    STUDENT (2) is the effective default for a normal signup; SUPER_ADMIN (5) is a tier above ADMIN (added in migration 0036_super_admin_role.sql) — super-admins are recognized as admins everywhere, plus they own the money/role-sensitive actions.

There is no user_role Postgres enum. Older versions of this doc described a CREATE TYPE "user_role" AS ENUM('student','admin') — that is wrong and was never in the current schema. The very first migration (0000_medical_iron_fist.sql) creates a roles table with a users.role_id FK; there is not a single CREATE TYPE statement in the migration history.

roleId is a global role. Per-context roles are separate and do not elevate global access:

  • Per-cycle: testers.role uses the TESTER_ROLES const — tester | lead | observer.
  • Per-team (legacy events): userTeams.role uses TEAM_ROLESowner | member.

The core QA domain

Catalog — who is testing what

The spine is a strict ownership chain: organizations → products → productVersions → testCycles, with testers joined in per cycle.

TablePurpose
organizationsThe tenant. Internal / client / sponsor / partner (ORGANIZATION_TYPES). Has a slug.
productsA product owned by an org. Carries the test url, repoUrl, a free-form discipline tag, an optional integration provider, and the payoutRequiresVerification policy default.
productVersionsA named/numbered version of a product, optionally attached to a cycle.
testCyclesThe unit of QA work: a scoped testing window against a product (+ optional version). Carries status, dates, lead, the open-to-join toggle, the cycle kind, and verifier-bounty config.
cycleDocumentsNotion-style markdown docs scoped to a cycle (brief / runbook / report / doc).
testersCycle membership. (testCycleId, userId) unique; per-row role (TESTER_ROLES) and a notify flag.

testCycles.kind (TEST_CYCLE_KINDS: standard | verification | feature) drives the tester view and which issues populate the cycle's queue. verification cycles re-test fixed bugs pulled from sourceCycleIds; feature cycles scope to featureLabels.

Execution — sessions and what testers find

TablePurpose
testRunsOne tester's testing session inside a cycle (in-progress / completed / abandoned). Holds environment metadata, notes, and an optional AI session recap.
issuesA filed bug / observation / task against a cycle (and optionally a run). The richest table: repro fields, severity/priority/status, payout tracking, verification-path routing, external-tracker linkage, AI suggestions, and optional test-case linkage.
issueCommentsThreaded discussion on an issue.
attachmentsPolymorphic media table. (targetType, targetId) points at an issue, comment, cycle, cycle document, or run (ATTACHMENT_TARGET_TYPES); mediaKind is image / video / file; the blob lives in S3 (s3Key).

Test cases — structured (vs. exploratory) testing

A reusable, product-scoped test-case library that issues can reference: testCases + testSuites/testSuiteCases (the library), cycleTestCases (a cycle's test plan, with a content snapshot so the plan is stable even if the source case changes), and testExecutions (append-only run history). An issue can link back to the testCaseId / testExecutionId that surfaced it.

Payouts — settling accepted bugs

TablePurpose
issuePayoutsThe payment ledger: one row per actual payment for an issue (amount in cents + currency + method + reference).
payoutBatchesA grouped settlement run (e.g. "1–15 May") so testers see one stamped paycheck and the period audit survives later voids. Per-payout rows reference their batchId (soft FK).
issueVerificationsOne row per tester pass over one bug in a verification cycle (verified / still_broken). A separate table so the audit survives status changes and the same bug can collect multiple attempts. Carries a snapshotted verifier bounty (payoutAmountCents) with its own PAYOUT_STATUSES lifecycle. (issueId, verificationCycleId, verifierUserId) unique.

Payout state on an issue itself lives in issues.payoutStatus / payoutAmountCents / payoutCurrency (the decision); issuePayouts records the money moving.

AI — agent provenance

TablePurpose
aiRunsAppend-only provenance for every LLM invocation: kind, polymorphic target, model, prompt version, status, token counts, cost, latency, and the structured output. The issue-intake agent's suggestions are mirrored onto issues.aiSuggestions with an aiIntakeRunId pointer back here.

Platform — operating the system

TablePurpose
apiKeysOrg-scoped programmatic access. Only the SHA-256 keyHash is stored (raw key shown once); carries scopes, optional cycleIds, a rateLimit, and isActive.
notificationsIn-app bell. One row per (user, event); nullable cycleId/issueId link the bell straight to the resource.
auditLogAppend-only "who did what, when, before/after" for payout decisions, status changes, role changes, cycle transitions. Fire-and-forget — never blocks the primary action.
rateLimitBucketsPostgres-backed fixed-window limiter; one row per (bucket key, window), incremented with an atomic upsert.
requestIdempotencyIdempotency keys for unsafe-to-repeat operations (the batch-payout endpoints): claim → process → cache response → replay on retry.

Key enums

All enums are const objects $type-tagged onto varchar columns (or integer FKs to a reference table for the legacy _enums.ts set). Values below are the real ones from the schema.

Enum (const)Column(s)Values
ROLESusers.roleIdroles.idadmin(1), student(2), guest(3), qa(4), super_admin(5)
ISSUE_TYPESissues.typebug, observation, task
ISSUE_SEVERITIESissues.severitycritical, high, medium, low, trivial
ISSUE_PRIORITIESissues.priorityurgent, high, normal, low
ISSUE_STATUSESissues.statusdraft, open, triaged, in_progress, fixed, verified, deferred, duplicate, rejected, cant_reproduce, wont_fix
ISSUE_BUG_TYPESissues.bugTypefunctional, visual, crash, performance, content, accessibility, security, other
ISSUE_VERIFICATION_PATHSissues.verificationPathstandalone, via_feature, none
ISSUE_VERIFICATION_RESULTSissueVerifications.resultverified, still_broken
PAYOUT_STATUSESissues.payoutStatus, issueVerifications.payoutStatuspending, info_requested, approved, rejected, paid, void
PAYMENT_METHODSissuePayouts.method, payoutBatches.methodbank_transfer, kaspi, cash, other
TEST_CYCLE_STATUSEStestCycles.statusplanned, active, review, closed, cancelled
TEST_CYCLE_KINDStestCycles.kindstandard, verification, feature
TESTER_ROLEStesters.roletester, lead, observer
TEST_RUN_STATUSEStestRuns.statusin_progress, completed, abandoned
CYCLE_DOCUMENT_KINDScycleDocuments.kinddoc, brief, runbook, report
ORGANIZATION_TYPESorganizations.typeinternal, client, sponsor, partner
TEST_CASE_STATUSEStestCases.statusdraft, active, deprecated
TEST_EXECUTION_STATUSEStestExecutions.statuspassed, failed, blocked, skipped
ATTACHMENT_TARGET_TYPESattachments.targetTypetest_cycle, issue, issue_comment, cycle_document, test_run
MEDIA_KINDSattachments.mediaKindimage, video, file
AI_RUN_KINDSaiRuns.kindintake_issue, run_summary, cycle_report
AI_RUN_STATUSESaiRuns.statusqueued, running, done, failed
API_KEY_SCOPESapiKeys.scopes (comma-joined)cycles:read, cycles:write, issues:read, issues:write, issues:triage, runs:write, payouts:read, payouts:write, analytics:read, ai:write, admin:read
INTEGRATION_PROVIDERSproducts.integrationProviderSlug, organizationIntegrations.providerSluglinear, jira, github_issues, anthropic
AUDIT_ACTIONSauditLog.actionissue.status_changed, issue.payout_decision, payout.batch_paid, cycle.status_changed, user.role_changed

For exact column lists, types, and the onDelete behaviour of every FK, read the domain file directly — the schema is the source of truth and this table deliberately doesn't duplicate it.

Migrations

Migrations are generated with drizzle-kit (dialect: 'postgresql', schema: './src/db/schema.ts', out: './src/db/migrations') and live in src/db/migrations/ with the journal under migrations/meta/.

There are 41 migrations, numbered 0000_* through 0040_last_roulette.sql (the journal in meta/_journal.json has 41 entries). The history is real and substantial — a few markers along the way:

  • 0000_medical_iron_fist.sql — initial schema, including the roles table.
  • 0028_verification_gate.sql — verification gate on payouts.
  • 0029_payout_batches.sql — batched settlement runs.
  • 0031_scale_indexes.sql — compound indexes for the hot list paths.
  • 0032_audit_log.sql / 0033_request_idempotency.sql — platform safety tables.
  • 0034_test_cases.sql — the structured test-case domain.
  • 0036_super_admin_role.sql — the 5th role.
  • 0040_last_roulette.sql — latest.

Connection, SSL & pooling

src/db/index.ts builds a pg.Pool and wraps it with drizzle(pool) (drizzle-orm/node-postgres). Highlights:

  • Pool tuning: max (DB_POOL_MAX, default 20), idleTimeoutMillis 30s, connectionTimeoutMillis 10s, and a per-session statement_timeout (DB_STATEMENT_TIMEOUT_MS, default 15s) set on every new connection.
  • SSL is mode-driven for managed Postgres: DB_CA_CERT (verify against a CA bundle), DB_SSL_MODE (verify/require/no-verify/allow/disable), and a fail-closed default (SSL on, rejectUnauthorized: true) in production when nothing else is set. Local/non-production with nothing set runs without SSL.

For provisioning, backups, the managed-provider cutover, and SSL operational detail, see ops/database.md.

On this page