Skip to main content

Schema

Fairway uses SQLite via modernc.org/sqlite (pure Go, no CGO). The database lives at the path configured by [fairway] db_path (default .fairway/state.db).

Fairway uses migration-managed tables plus schema_migrations for migration tracking. Task hierarchy (epics, stories, subtasks) lives in task_definitions via a self-referential parent_id — see hierarchy.md. Checkpoints attach append-only operating notes to tasks; see checkpoints.md.

Project scope

Every table carries a project_id TEXT NOT NULL column. In v1 SQLite, each DB holds exactly one project_id value (set from [fairway] project_name at DB open). In a future Postgres adapter, one DB will hold many project_id values — no migration needed to add the column because it is already there.

PKs and FKs that include project_id are explicit per table below. The store layer threads project_id through every read and write; callers never pass it.

Multi-project visibility on a single user's machine is still provided at the dashboard layer via ATTACH DATABASE over a registry; see multi-project.md.

SQLite remains the default Fairway store. A future Postgres/server-backed store is for shared team write coordination, not for dashboard caching or a second read-model truth source. The Postgres path must preserve the same schema ownership boundaries, command semantics, and project_id scoping described here; see postgres-adapter.md for the assessed deployment model, cutover requirements, and compatibility harness.

Track memory lifecycle

track_memory is replace-by-key curated context with accountable owner, review_by, disposition, promotion_target, canonical_commit, and superseded_by_track_id projection fields. New active rows require at least one existing checkpoint, evidence, or review source ID.

track_memory_lifecycle is append-only audit history for explicit disposition changes. It records prior and next disposition, reason, promotion or supersession references, actor, and timestamp. Reconciliation and dashboard views derive lifecycle debt from these rows; there is no second memory or wait store.

Tables

task_definitions

Slowly-changing task metadata. One row per task. Most fields are mutable via fairway update — see "Mutability" below.

ColumnTypeNotes
project_idTEXT NOT NULLProject this task belongs to. Immutable.
idTEXT NOT NULLStable task identifier, e.g. T-042. Unique within a project. Immutable.
parent_idTEXTSelf-referential FK for hierarchy (epics → stories → tasks). NULL = root. Mutable (reparenting).
kindTEXTOptional label (epic, story, task, bug, spike). Validated against [task_kinds] allowed when configured. Mutable.
titleTEXT NOT NULLShort human-readable title. Mutable.
roleTEXT NOT NULLRole that owns this task. Mutable (handoff updates task_state.owner, not this).
notesTEXTLong-form description, acceptance criteria, links. Mutable.
acceptance_checksTEXTJSON array of opaque strings. Mutable.
dependenciesTEXTJSON array of task IDs that must reach a terminal state before this task is ready. Mutable.
priorityINTEGERUrgency. Lower = more urgent. NULL = unprioritized. Validated against [task_priorities] when configured. Cross-cutting (overrides epic boundaries in sort).
sequenceINTEGERSuggested order among siblings (same parent_id). Lower = earlier. NULL = unsequenced. Soft signal, not a gate.
profileTEXTOptional workstream profile name. Validated against [[workstream_profiles]] when configured.
owning_domainTEXTOptional architecture/domain owner label, e.g. platform, billing, identity.
owning_layerTEXTOptional layer label, e.g. api, service, frontend, guard, release.
source_pathsTEXTJSON array of source paths relevant to the task.
target_pathsTEXTJSON array of intended target paths or artifacts.
review_domainsTEXTJSON array of review domains expected for this task.
tagsTEXTJSON array of generic cross-cutting tags. Supports simple tags such as production-readiness and key:value tags such as environment:staging.
risk_levelTEXTOptional risk label, e.g. low, medium, high.
migration_typeTEXTOptional migration/refactor type, e.g. facade, boundary-guard, ownership-map.
created_atDATETIME NOT NULLImmutable.
created_byTEXTOS user or agent identifier. Immutable.
updated_atDATETIME NOT NULLTouched on any mutable-field change.

Primary key: (project_id, id). FK: (project_id, parent_id) → task_definitions(project_id, id).

Indices:

  • (project_id, parent_id) — descendant traversal.
  • (project_id, status, priority, sequence, created_at) via join with task_state — backlog sort hot path (see hierarchy.md and dashboard.md for the sort order).

Mutability

Three orthogonal ordering signals: dependencies (hard gate — task not ready until deps terminal), priority (soft, cross-cutting urgency), sequence (soft, within-siblings order). All three are mutable.

An audit table task_definitions_changes may come in v0.2 if drift becomes a debugging pain. For v0.1, the audit trail is updated_at plus the git history of any YAML/JSON imports.

See hierarchy.md for the tree model, the spawn command, granularity rules, and epic rollup semantics.

task_state

Mutable per-task execution state. One row per task.

ColumnTypeNotes
project_idTEXT NOT NULL
task_idTEXT NOT NULL
statusTEXT NOT NULLMust be in the configured [states] allowed.
ownerTEXTRole currently responsible.
claimantTEXTOS user or session identifier holding the claim.
branchTEXTBranch where work is happening.
claimed_atDATETIME
completed_atDATETIME
commit_shaTEXTCommit that satisfied the task, when done.
review_requiredBOOLEAN NOT NULL DEFAULT 0Set by fairway route review.
review_statusTEXTDenormalized latest review status: not_required / pending / approved / changes_requested. CLI and dashboard detail views may display partial_approval when this value is approved but required review_domains are still missing.
reviewerTEXTLatest routed or recorded reviewer.
reviewed_atDATETIMELatest review timestamp, when any.
review_noteTEXTLatest review summary, when any.
updated_atDATETIME NOT NULL

Primary key: (project_id, task_id). FK: (project_id, task_id) → task_definitions(project_id, id).

Indices:

  • (project_id, owner, status) — hot path for "what is each role doing?" on the dashboard.
  • (project_id, status) — backlog views.
  • (project_id, claimant) — session reconciliation.

task_state_history

One row per state transition. Append-only.

ColumnTypeNotes
idINTEGER PK
project_idTEXT NOT NULL
task_idTEXT NOT NULL
from_statusTEXTNULL on initial insert.
to_statusTEXT NOT NULL
from_ownerTEXTPrevious owner / responsible role.
to_ownerTEXTNew owner / responsible role.
from_branchTEXTPrevious branch.
to_branchTEXTNew branch.
from_commit_shaTEXTPrevious commit SHA.
to_commit_shaTEXTNew commit SHA.
command_sourceTEXTCLI command or integration that created the row.
actorTEXT NOT NULLActive session ID when known, otherwise <os_user>@<host>.
reasonTEXTOptional human note.
atDATETIME NOT NULL

FK: (project_id, task_id) → task_state(project_id, task_id). Index: (project_id, task_id, at) for the task detail page.

task_handoffs

Directed handoff between roles.

ColumnTypeNotes
idINTEGER PK
project_idTEXT NOT NULL
task_idTEXT NOT NULL
from_roleTEXT NOT NULL
to_roleTEXT NOT NULL
payloadTEXTInline text or path to a file.
commit_shaTEXTCommit being handed off, if any.
changed_filesTEXTHuman summary or newline-separated file list.
commandsTEXTAcceptance commands run before handoff.
resultsTEXTSummary of command results.
risksTEXTResidual risks.
blockersTEXTKnown blockers.
next_stepTEXTRecommended next slice of work.
acknowledged_atDATETIMEWhen to_role acknowledged.
created_atDATETIME NOT NULL

FK: (project_id, task_id) → task_state(project_id, task_id). Index: (project_id, to_role, acknowledged_at).

task_evidence

Artifact paths and result classifications.

Evidence rows are append-only execution facts. Corrections should add a new evidence row, checkpoint, review, or superseding task note rather than editing or deleting historical evidence out of band. Fairway stores metadata and references here, not artifact contents. Use fairway provenance manifest to hash selected exported bundles or artifacts when a release/audit packet needs tamper-evidence. fairway audit export projects these existing rows in stable id order into fairway.sovereign-audit-record.v1 JSONL. The export includes actor, action, project, task, created-at, and a SHA-256 of detail; raw detail content is not exported. Each row binds the previous row hash so the chain remains stable when export policy, Fairway version, or trusted-time source changes. A customer-signed fairway.sovereign-audit-export.v1 manifest binds the record file, chain head, retention/legal-hold metadata, trusted-time evidence digest, source version, and either a genesis marker or the previous externally retained checkpoint. This is a derived export, not a second audit store, and does not mutate the audit_events source of truth.

Offline distribution is also file-based rather than a database store. fairway.offline-distribution-manifest.v1 binds current and rollback release identity, required platform archives and verifier binaries, typed local assets, lifecycle scripts, file modes, sizes, and SHA-256 digests. A detached fairway.offline-distribution-signature.v1 Ed25519 signature binds the exact manifest. fairway.offline-distribution-verification.v1 is a derived, read-only verification report. None of these schemas mutates task, release, deployment, or certification state.

ColumnTypeNotes
idINTEGER PK
project_idTEXT NOT NULL
task_idTEXT NOT NULL
handoff_idINTEGEROptional FK to task_handoffs(id).
command_textTEXTCommand or check that produced this evidence.
resultTEXTpass / fail / partial / skipped / blocked / NULL.
artifact_pathTEXTScreenshot, log, transcript, report, or other artifact path.
artifact_typeTEXTOptional display hint, e.g. log, screenshot, video, browser-trace, uat, coverage, report. UX media types are stored as artifact references only and must be redacted before recording.
duration_secondsINTEGEROptional elapsed time for timing reports.
notesTEXT
created_atDATETIME NOT NULL

FK: (project_id, task_id) → task_state(project_id, task_id).

task_decisions and task_decision_assessments

task_decisions is the append-only curated explanation for a material task choice. It stores the decision, trigger, alternatives, chosen option, reason, added scope, risk, validation references, supporting fact references, optional superseded decision id, author, and creation time. Structured lists are JSON text for SQLite/Postgres compatibility. A unique partial index permits only one direct replacement for a superseded row.

task_decision_assessments appends independent accepted or insufficient quality findings. The task owner or claimant cannot assess their own decision. The read model derives draft when no assessment exists and superseded when a later decision replaces the row. Earlier decisions and assessments are never rewritten or deleted by the command surface.

Decision text is privacy-bounded and cannot grant approval, merge, deploy, credential, release, public-exposure, or live-operation authority. An accepted decision means the explanation is concrete and fact-consistent; normal Fairway review, evidence, merge, deploy, and release gates remain separate.

task_reviews

Review records.

ColumnTypeNotes
idINTEGER PK
project_idTEXT NOT NULL
task_idTEXT NOT NULL
reviewerTEXT NOT NULL
verdictTEXT NOT NULLapprove / changes / reject.
reviewed_commit_shaTEXTCommit reviewed, if applicable.
route_reasonTEXT
notesTEXT
created_atDATETIME NOT NULL

FK: (project_id, task_id) → task_state(project_id, task_id).

Constraint (enforced in code): reviewer != task_state.claimant.

task_reviews is the audit log. The review columns on task_state (review_required, review_status, reviewer, reviewed_at, review_note) are denormalized/materialized dashboard fields. Every review insert updates task_reviews and the corresponding task_state review columns in the same transaction. Readers may use the denormalized columns for current state, but historical review questions must query task_reviews. Verdicts map to current review status as approveapproved; changes or rejectchanges_requested.

Required review-domain completeness is evaluated from task_reviews plus task_definitions.review_domains; it is not encoded directly in task_state.review_status. User-facing CLI/dashboard detail views should not summarize a latest approved review as domain-complete approval while required domains are missing; they render that case as partial_approval.

agent_sessions

Lifecycle for a single agent process attached to a lane.

ColumnTypeNotes
project_idTEXT NOT NULL
idTEXT NOT NULLSession identifier (UUID or role-pid-startts).
roleTEXT NOT NULL
laneTEXTOptional lane identifier when multiple execution slots share a role; see concepts.md.
worktree_pathTEXTWorktree path for status and attach affordances.
branchTEXTBranch active when the session was recorded.
session_backendTEXTtmux, zellij, shell, or another adapter label.
providerTEXTInformational provider label, e.g. codex, claude, gemini, shell.
session_nameTEXTHuman-readable backend session name.
task_idTEXTTask associated with the session, when known.
pidINTEGEROS PID.
tmux_paneTEXTe.g. agents:0.2.
transcript_pathTEXTOptional path reference; transcript contents are not stored in DB.
statusTEXT NOT NULLstarting / running / ended / failed / stale.
started_atDATETIME NOT NULL
last_heartbeat_atDATETIME
ended_atDATETIME
exit_codeINTEGERProcess exit code, when known.
end_reasonTEXTnormal / reconciled / crashed / NULL.

Primary key: (project_id, id). Index: (project_id, role, ended_at) — find the live session for a role.

task_checkpoints

Append-only operating checkpoints for epics, stories, side tracks, and watcher work.

ColumnTypeNotes
idINTEGER PK
project_idTEXT NOT NULL
task_idTEXT NOT NULL
stateTEXT NOT NULLplanned / active / awaiting_input / review / done / parked / abandoned.
ownerTEXTRole or lane responsible for the checkpoint.
target_close_byDATEOptional date for stale-track checks.
summaryTEXT NOT NULLCurrent operating summary.
artifact_pathTEXTOptional evidence link.
created_atDATETIME NOT NULL

FK: (project_id, task_id) → task_definitions(project_id, id). Index: (project_id, task_id, created_at). Index: (project_id, state, target_close_by).

server_write_idempotency

FW-271 adds a small idempotency ledger for the shared-team write API pilot. FW-272 extends that ledger to guarded status/review writes. It is not a second evidence, checkpoint, status, or review store; it records retry metadata for accepted API writes so network clients can safely replay the same request.

ColumnTypeNotes
project_idTEXT NOT NULL
command_familyTEXT NOT NULLrecord:evidence, record:checkpoint, set:status, or record:review.
idempotency_keyTEXT NOT NULLClient-supplied retry key.
actorTEXT NOT NULLRedacted actor/fingerprint, never a raw token.
roleTEXT NOT NULLCommand-scoped role used for authorization.
auth_sourceTEXT NOT NULLIdentity source, for example api_token.
task_idTEXT NOT NULL
payload_digestTEXT NOT NULLDigest of the accepted structured payload.
result_kindTEXT NOT NULLevidence or checkpoint.
result_idINTEGER NOT NULLInserted append-only fact row ID.
created_atDATETIME NOT NULL

Primary key: (project_id, command_family, idempotency_key). Index: (project_id, task_id, created_at).

A replay is accepted only when actor, role, auth source, task, command family, and payload digest match the original row. Mismatched replay fails closed. The table stores payload digests and resulting row IDs, not raw request bodies, prompts, transcripts, raw tool bodies, generated content, credentials, or secrets.

provider_usage_events

Append-only provider usage attribution. This table stores normalized counts and metadata only. It must not store prompts, transcripts, secrets, provider inputs, provider outputs, messages, or generated content. Missing numeric values stay NULL; Fairway treats them as unknown rather than zero.

ColumnTypeNotes
idINTEGER PK
project_idTEXT NOT NULL
providerTEXT NOT NULLProvider label such as codex, claude, gemini, tmux, or shell.
external_session_idTEXTProvider-side session/thread/run id, when known.
session_idTEXTFairway agent_sessions.id, when known.
task_idTEXTFairway task receiving attribution. Nullable for provider runs not yet mapped to a task.
roleTEXTRole or lane receiving attribution.
phaseTEXTOptional work phase such as implementation, review, ci, deploy, or uat.
sourceTEXT NOT NULLprovider_reported, derived_snapshot, manual, or unknown.
confidenceTEXT NOT NULLexact, estimated, or unknown.
started_atDATETIMEMeasured usage window start.
completed_atDATETIMEMeasured usage window end.
started_token_snapshotINTEGEROptional provider running total at start.
completed_token_snapshotINTEGEROptional provider running total at completion.
input_tokensINTEGEROptional provider-reported input tokens.
cached_input_tokensINTEGEROptional provider-reported cached input tokens.
uncached_input_tokensINTEGEROptional provider-reported or derived uncached input tokens.
output_tokensINTEGEROptional provider-reported output tokens.
reasoning_tokensINTEGEROptional provider-reported reasoning tokens.
total_tokensINTEGEROptional provider-reported or snapshot-derived total tokens.
elapsed_secondsINTEGEROptional elapsed time.
modelTEXTOptional provider model label.
metadata_jsonTEXTOptional small JSON object for non-sensitive metadata.
created_atDATETIME NOT NULL

FK: (project_id, task_id) → task_definitions(project_id, id).

Indices:

  • (project_id, task_id, created_at) — task detail usage timeline.
  • (project_id, provider, created_at) — provider rollups.
  • (project_id, role, created_at) — lane rollups.
  • (project_id, created_at) — daily reports.

work_batches

Execution and validation plans for related tasks that share one branch, worktree, CI/deploy-run, review path, and evidence set. Tasks remain the accountability unit; batches are the implementation and validation unit.

ColumnTypeNotes
project_idTEXT NOT NULL
idTEXT NOT NULLStable batch identifier, e.g. BATCH-001.
titleTEXT NOT NULLHuman-readable batch title.
branchTEXTShared implementation branch.
worktree_pathTEXTShared worktree path.
validation_commandsTEXTJSON array of commands expected to validate the batch.
review_domainsTEXTJSON array of review domains expected for the shared work.
rollback_criteriaTEXTCriteria for reverting or backing out the batch.
split_criteriaTEXTCriteria for splitting the batch when failure diagnosis or ownership diverges.
expected_ciTEXTExpected CI/deploy-run description.
deploy_run_idTEXTLinked deploy-run id, when known.
pipeline_idTEXTLinked CI pipeline/run id, when known.
created_atDATETIME NOT NULL
updated_atDATETIME NOT NULL

Primary key: (project_id, id).

work_batch_tasks

Membership join table from work batches to granular Fairway tasks.

ColumnTypeNotes
project_idTEXT NOT NULL
batch_idTEXT NOT NULL
task_idTEXT NOT NULL
created_atDATETIME NOT NULL

Primary key: (project_id, batch_id, task_id). FKs:

  • (project_id, batch_id) → work_batches(project_id, id).
  • (project_id, task_id) → task_definitions(project_id, id).

Index: (project_id, task_id) — task detail batch lookup.

work_batch_evidence

Batch-level evidence records. fairway batch evidence can also map evidence to each member task by inserting corresponding task_evidence rows with a work_batch=<batch-id> note.

ColumnTypeNotes
idINTEGER PK
project_idTEXT NOT NULL
batch_idTEXT NOT NULL
command_textTEXTShared validation command.
resultTEXTpass / fail / partial / skipped / blocked / NULL.
artifact_pathTEXTPipeline URL, deploy-run, log, report, or local artifact reference.
artifact_typeTEXTOptional display hint such as ci, deploy, uat, or work-batch.
notesTEXT
created_atDATETIME NOT NULL

FK: (project_id, batch_id) → work_batches(project_id, id). Index: (project_id, batch_id, created_at).

Migration strategy

  • One SQL file per migration in internal/store/migrations/, named 001_init.sql, 002_*.sql, ...
  • Embedded via //go:embed.
  • A schema_migrations(version INTEGER PK, applied_at DATETIME) table tracks applied versions. (No project_id — migrations are per-DB, not per-project.)
  • Migrations are forward-only in v1. fairway db backup runs automatically before any migration beyond 001_init.sql.

Design notes

Why project_id everywhere even in single-project SQLite? So the schema is portable to a shared backend (Postgres) without a row-rewrite migration. The marginal cost in v1 is ~8 short string columns and a WHERE project_id = ? clause on every read — both hidden behind the store layer.

Why split definitions from state? Same reason a user table is split from a session table: definitions are referenced by foreign keys and rarely change; state churns.

Why an explicit task_state_history? SQLite has no built-in temporal tables. The audit trail is a first-class queryable surface for the dashboard's activity feed.

Why does agent_sessions carry tmux pane? So the dashboard can render a "click to attach" affordance. NULL when tmux is not in use.

Why evidence has both command text and artifact path. Consumer use showed that completed work needs command-level proof even when there is no durable file artifact. Artifact paths remain optional references; large logs, screenshots, and transcripts stay out of the DB.

Why keep checkpoints after dropping track_checkpoints. Fairway does not need a separate track identity table because epics/stories already represent bounded work. It still needs append-only operating decisions for active, parked, awaiting-input, and watcher-style work; task_checkpoints provides that without creating a second task hierarchy.

Write Semantics

Claim Concurrency

SQLite claim must be atomic and deterministic:

  1. Open BEGIN IMMEDIATE so the writer lock is acquired before reading claimable state.

  2. Validate the task is claimable in the same transaction.

  3. Run a guarded update, for example:

    UPDATE task_state
    SET status = 'in_progress',
    owner = ?,
    claimant = ?,
    branch = ?,
    claimed_at = ?,
    updated_at = ?
    WHERE project_id = ?
    AND task_id = ?
    AND status IN ('todo', 'blocked')
    AND claimant IS NULL;
  4. If zero rows were updated, rollback and return ErrAlreadyClaimed or the more specific validation error.

  5. Insert the task_state_history row in the same transaction as the successful update.

  6. Commit.

Tests must prove two concurrent claim attempts produce exactly one winner and one loser.