Tack Architecture
Updated: 2026-10-04
System shape
Tack is one Next.js App Router application and one PostgreSQL database:
Browser
├─ private sign-in
├─ server-rendered board snapshot
├─ optimistic local interactions
└─ focus/interval refresh
↓
Next.js pages and route handlers
├─ Better Auth sessions
├─ page/API authorization
└─ narrow issue/label mutations
↓
IssueStore + node-postgres
↓
PostgreSQL
The app remains one service with PostgreSQL. Optional configured OIDC providers (including Cognito User Pools) supplement local authentication. There is no separate API service, worker, cache, queue, WebSocket subsystem, or object storage.
Runtime boundaries
src/app/page.tsxrequires a session and reads the initial board snapshot.src/components/board.tsxowns DnD, drawers, filters, optimistic updates, rollback, freshness, and Undo.src/app/api/issues/**requires any authenticated account.src/app/api/labels/**requires an administrator.src/app/api/boards/**allows authenticated listing and administrator creation/removal.- Issue snapshots and ordering are scoped by board ID; members can transfer active or archived cards across boards.
src/app/api/exportgives administrators JSON or CSV data-portability downloads.src/app/api/auth/[...all]exposes Better Auth's maintained session and administrator endpoints.src/app/team/page.tsxandsrc/components/team-manager.tsxprovide the deliberately small account-administration surface.src/lib/auth-providers.tsvalidates server-side provider configuration and trusted UserInfo.src/lib/auth.tsdefines local and optional OIDC authentication, database sessions, the admin plugin, and Tack's extra user presentation fields.src/lib/auth-guards.tsperforms page and request authorization close to the data access boundary.src/lib/issue-store.tsowns issue invariants and PostgreSQL transactions.src/lib/database.tsowns the connection pool, transaction helper, advisory migration lock, and ordered migrations.migrations/postgres/is the deployed schema source of truth.
The root migrations/001_initial.sql and 002_team_context.sql files are
retained only as the historical SQLite prototype schema. They are not applied
by the current runtime.
Agent access
/settings/api-keys and /api/keys/** require a browser session and current active account. Users own their key metadata; administrators may review metadata and revoke keys across users. Better Auth's API-key plugin is pinned to 1.6.25 with hash-only storage and browser-session emulation disabled. Its HTTP management endpoints are disabled; Tack's handlers validate origin, scopes, boards and expiry.
/api/v1/** requires a Bearer API key, never a cookie fallback. It verifies once, then rechecks current owner eligibility and scopes inside the operation transaction. Existing browser and account APIs reject Authorization headers. No key authorizes account, board, label administration or workspace export.
agent-service.ts wraps shared IssueStore operations in one transaction, reusing the store's global ordering lock. Every card update advances a database revision, including browser writes. Agent edits check the revision under lock. Every agent mutation also writes a 24-hour idempotency record atomically; retries check current board access before returning cached data. Credential metadata is locked while operations run so revocation cannot interleave halfway through an authorized transaction. Reads and mutation results remove private assignee fields and unauthorized former-board aliases.
The optional tools/tack-mcp.ts runs locally over stdio using the pinned official SDK 1.32.0. It uses a fixed origin and environment key to call the agent API; it has no database access and opens no MCP network listener. See agent access for the endpoint/tool contract, rate limits, cache/audit behavior, and LAN trust setup.
Responsive board interaction
The board uses four columns at 1180 CSS pixels and above. Below that width, explicit status buttons select one column; a filter signature scopes that choice. A new filter view may initially select a matching column, while the user's subsequent choice is honored even when its result count is zero. Changes to the shared snapshot do not override an explicit choice for the current filter view.
Quick capture precedes each column's list. Creation and the C/New card shortcut retain the view's filters. If the newest created card is hidden by those filters, a persistent notice identifies its destination and offers Show card, Clear filters, and dismiss. Showing the card preserves the filter URL. Clearing filters from this notice also selects the destination column.
Phone filter controls are disclosed on demand, with active values visible as removable chips. Archive, Team, label administration, and sign-out live in a native details disclosure. Drawer focus returns to its originating control, or the visible Board options summary when the card has no visible trigger.
Shared freshness and recovery
The shared refresh state, pending-write count, and expired-session flag are independent. Completing a mutation cannot mark a failed shared refresh healthy. A successful applied shared read records its checked-at time; refresh requests are single-flight and still honor mutation-version and open-panel guards. Drawers explicitly explain their paused shared updates.
Move failures restore the serialized operation's prior local snapshot and column, then retain an explicit retry/check/dismiss action. These are acknowledgements and local recovery controls, not an exactly-once protocol. Capture failures keep the title and expose Check board before a deliberate resubmission. New-card creation is never automatically retried after an uncertain response.
SessionRecovery opens a validated local sign-in destination in another tab.
Quick capture, labels, and Team forms stay mounted in the original tab. The user
returns and retries; passwords are not persisted to browser storage. Issue draft
recovery continues to use the separate U1 hook and its same-tab return path.
Team feedback is keyed to its account or create form. One action runs at a time; controls remain disabled during the subsequent server refresh. Password Cancel removes that form and restores trigger focus. Label deletion has an inline, explicit consequence/Cancel step, and pending label operations guard closing. Archive failures and success actions live outside the scrolling list or note. Board failure notices remain available after leaving a panel, while the panel owns visible recovery during the modal interaction.
Identity and authorization
Better Auth 1.6 owns four tables: "user", session, account, and
verification. The admin plugin extends users with role/ban fields and
sessions with impersonation metadata. Tack extends users with initials and
one restrained avatar color.
OIDC authorization-code sign-in uses PKCE and verified UserInfo. Local authentication remains available. New provider users are denied by default; configured provisioning creates members. Provider identity does not grant an administrator role. See authentication setup for configuration, pinned-version callback paths, and the live-provider verification boundary.
The product exposes two roles:
admin— normal board access plus account and label managementmember— normal board access
Better Auth's internal non-admin role is named user; Tack maps it to
member at the UI/domain boundary. Public sign-up is disabled. The bootstrap
script temporarily enables server-side sign-up only inside its own process,
then marks the configured account as an administrator.
Authorization is not inferred from client controls. Protected server pages
redirect unauthenticated users, issue APIs return 401, label APIs return
401 or 403, the export API returns 401 or 403, and Better Auth enforces
its own administrator endpoints.
Passwords are hashed by Better Auth's maintained scrypt implementation. Sessions are database-backed and use secure HTTP-only cookie behavior supplied by Better Auth.
Data portability
The administrator-only export boundary reads the same IssueStore snapshot as
the board plus disabled account profiles needed to resolve historical
assignees. The version-2 JSON form contains users, labels, all boards (including removed/reserved IDs), per-board number counters, and every active or archived issue with stable IDs, current keys, former-key aliases, and relationships. It is the lossless portable
snapshot.
The CSV form emits one row per issue and resolves assignee and label names for inspection in a spreadsheet. Every cell is quoted and user-entered values that could be interpreted as formulas are prefixed with an apostrophe.
Neither format reads or serializes Better Auth's account, session, or
verification records. Export responses are marked no-store and nosniff.
Data model
user
id, name, email, emailVerified, image
role, banned, banReason, banExpires
initials, color, createdAt, updatedAt
session
id, token, userId, expiresAt
ipAddress, userAgent, impersonatedBy, createdAt, updatedAt
account
credential provider record and password hash
verification
expiring verification values used by the auth library
issues
id random internal UUID
board_id immutable short board ID, foreign key to boards
public_number monotonic number within this board
title required, trimmed, 1–180 characters
description optional Markdown, up to 20,000 characters
status backlog | ready | in_progress | done
position zero-based integer within an active status
created_at
updated_at
archived_at null while active
assignee_id nullable user reference
labels
id
name unique, case-insensitive, 1–24 characters
color one of six restrained workspace colors
created_at
issue_labels
primary key (issue_id, label_id)
boards stores immutable short IDs, display names, next issue numbers, and removal timestamps. issue_keys maps every allocated public key to its stable issue ID. Removed board IDs remain reserved. app_meta retains the historical single-board counter; new allocation uses boards. schema_migrations records applied PostgreSQL migrations.
Persistence invariants
- Each board allocates
BOARD-Nnumbers that are never reused. Transfers allocate a new destination key; all old keys remain resolvable aliases. - Existing TCK data migrates without renumbering; only empty boards can be removed, and at least one board must remain.
- Validation and database constraints both require a nonblank title and valid status.
- Active positions are contiguous and zero-based within each board/status.
- Create, move, archive, and restore acquire one transaction-scoped advisory lock for board ordering.
- Each transaction uses one checked-out PostgreSQL client for
BEGIN/COMMIT/ROLLBACK. - A cross-column move closes the source gap and opens the target position.
- Archive preserves the last position for a best-effort restore.
- Normal UI actions never permanently delete issues.
- Issue patches modify only fields present in the request.
- Assignee and label references are validated before commit.
- Disabled users remain hydratable on historical cards but are not offered for new assignments.
- Removing a label cascades only through
issue_labels.
The ordering algorithm is O(n) within a board/status. The existing global transactional ordering lock also serializes board creation/removal and cross-board transfers.
Client interaction model
- The server provides active issues, archived issues, active members, and labels in one initial snapshot.
- Create waits for its assigned public key before inserting the card.
- Text/metadata edits and moves update local state optimistically. Archive and restore wait for confirmation so their recovery context remains available.
- A browser-local promise queue serializes board writes and synchronously updates the snapshot reference before the next operation. This prevents one local snapshot rollback from overwriting another in-flight local operation. It is not a distributed queue or cross-user same-field conflict mechanism.
- Failed field patches roll back only the requested fields. Failed moves restore the snapshot captured when that serialized operation starts; archive/restore failures retain their unchanged collection and visible recovery context.
- Board state lives in
?board=ENG, card state inissue=ENG-N; filters live inq,assignee, andlabel. Historical issue links resolve before the board snapshot is selected. use-issue-draft.tsowns title/description drafts. Saves start after a 700 ms pause; a single in-flight writer drains newer edits before confirming Saved. Close, Archive, movement, and metadata operations flush text first.- Drawers use native modal dialogs with explicit focus wrapping. The covered board is inert; users close/save or explicitly discard before switching cards.
- Unsaved text is retained in session storage per account/card for recovery in the same tab, with a 24-hour validity limit, and cleared on save/discard. A recovered draft requires review before saving. Storage failure keeps the editor open and explains the manual-copy fallback. This is not offline sync.
- Shared destinations pass through sign-in using an allowlisted local return path. Missing card links produce an actionable drawer while retaining filters.
- The editor distinguishes locally touched fields from server-refreshed fields.
- The board refreshes every 12 seconds and on focus, pausing during drawers, drag, or active mutations.
Deployment
Host reverse proxy / TLS
↓
Docker Compose
├─ Tack application
│ migrate → bootstrap admin → start
└─ PostgreSQL 18
└─ named persistent volume
Compose waits for PostgreSQL's health check before starting Tack. Both services
restart unless stopped. The application exposes /api/health, which checks a
real database query. PostgreSQL is bound to host loopback; the application port
is available to a host reverse proxy.
The migration runner serializes competing starts with a PostgreSQL advisory lock. The first shared release targets one application replica.
Backups use pg_dump --format=custom; restore uses pg_restore after replacing
the configured database. Both commands run the PostgreSQL tools from the
database container so host tool versions cannot drift from the server.
Accessibility and visual model
The board retains banner, main, region, heading, form-control, dialog, and status semantics. DnD has pointer, touch, and keyboard sensors, while every card also exposes a native status selector and explicit reorder buttons. Focus, live announcements, reduced motion, forced colors, and mobile single-column behavior remain part of the global design contract.
The visual thesis is a calm worktable: warm neutral canvas, off-white working surfaces, serif display type, sans-serif controls, monospaced keys, and restrained status colors. Account administration uses the same visual language and stays secondary to the board.
Automated checks reduce common accessibility defects but do not establish full WCAG conformance.