Skip to content

Authorship Evidence — Human Creative Direction Record

Issue: #551 · Date: 2026-05-03 · Status: Canonical


Purpose

This document preserves a record of meaningful human creative decisions made throughout VERA's development. It exists because copyright protection for AI-assisted code requires evidence that a human author made genuine creative choices — choosing architecture, deciding what to reject, restructuring output.

Directing an AI model toward an objective is not sufficient. Directing how the work is constructed is. This file documents the latter.


Foundational Architectural Decisions

Multi-tenant model via AsyncLocalStorage (not request context threading)

Decided to carry tenant context through the entire async call chain using AsyncLocalStorage rather than threading a tenant parameter through every function signature. This decision was made after rejecting the parameter-threading approach as brittle — a single missing parameter in a deeply nested call silently reads from the wrong tenant. The ALS approach makes context implicit and fail-closed: if no tenant context is set, the call throws rather than silently serving another tenant's data.

Two-spreadsheet architecture (database sheet + reporting sheet)

Decided to separate the database Google Sheet (owned exclusively by the service account) from the reporting Google Sheet (owned by the agency, shared with clients via IMPORTRANGE). This was a deliberate rejection of a single-sheet design where the bot and clients would both read from the same file. The separation means the service account never needs access to the client-visible file, and the bot never has to sanitise its own data for client visibility.

Positional column reading (index-based, not header-name-based)

Decided to read every Google Sheets tab by column position rather than by header name. This was a deliberate choice to avoid runtime header parsing, which would make the bot sensitive to minor label changes in a spreadsheet it cannot control. The trade-off is a strict schema contract documented in CLAUDE.md — any column reordering silently breaks reads. The contract is enforced by test fixtures, not by runtime header detection.

Write-ahead log (WAL) for Sheets propagation delay

After observing Google Sheets' server-side read/write propagation delay causing a newly appended row to be invisible on the immediately subsequent read, the decision was made to implement a client-side WAL in lib/dal.js rather than retry-looping reads or sleeping after writes. The WAL approach was chosen over a sleep-then-read retry because the propagation delay is non-deterministic — a sleep that works today may fail tomorrow. Three tables are covered: 07. Budgets, 09. Conversations, 01. Time Entries.

DAL dispatcher pattern (no raw Sheets writes in callers)

Decided to route all reads and writes through a single dispatcher in lib/dal.js rather than calling googleapis.spreadsheets.values.append directly from business logic. This decision was made when Postgres shadow writes were introduced — without the dispatcher, adding a second write target to 47 call sites would have been individually error-prone. The invariant ("no raw Sheets writes outside lib/dal.sheets.js") was enforced retroactively across five clusters of cleanup PRs (#483–#487) before any Postgres read flips.

Postgres migration strategy (fail-closed env gate, not code gate)

Decided to gate Postgres writes on PG_SHADOW_WRITE_TENANTS — an env var listing tenant IDs — rather than on a per-tenant database column or code flag. This means adding a tenant to Postgres is a deploy-time operation, not a runtime one, and an env var that is absent (or empty) means zero Postgres writes anywhere. The fail-closed default was chosen explicitly: a misconfigured deploy is better discovered as a missing write than as an undetected dual-write inconsistency.

Permission model (three tiers: Owner / Manager / User)

Decided to implement three permission tiers rather than a flat or RBAC-style model. The tiers were designed around what each person in an agency actually needs to know: Owners see dollars and margins; Managers see percentages and their assigned projects; Users see only their own time entries. The decision to hide deadlines entirely from Users was made after considering that surfacing project timelines to individual contributors creates pressure without context — managers see deadlines, users see allocation envelopes.

Allocation envelope model (not project membership)

Decided that project "assignment" means having an active hour envelope in 07. Budgets, not membership in a separate project-users table. This removes a whole class of sync problem: there is no state to get out of sync between "is this person on the project?" and "does this person have hours to spend?". The envelope is the assignment. Anyone can log time to any active project — the envelope is for planning visibility, not access control.

Vendor users vs contractor users

Decided to model the fte/contractor/vendor distinction on employment_type rather than a separate is_vendor boolean or a separate users table. The three values encode: does this person log their own time (fte/contractor), or does a PM log for them (vendor)? Do they receive allocation envelopes (fte/contractor), or are they tracked as a fixed cost liability (vendor)? This was a deliberate rejection of a separate vendors table that would have duplicated the user schema.

Slug-based dashboard authentication (over token + query param)

Decided to introduce a /d/:slug vanity URL as the recommended dashboard auth mechanism instead of the original ?token= query parameter. The slug serves as both the route and the credential — there is nothing else to share. The decision to use crypto.timingSafeEqual for comparison was made because timing attacks on token comparison are real and easy to avoid.


Rejected Approaches (Documented as Evidence of Design Choice)

The following approaches were explicitly evaluated and rejected. Rejection decisions represent meaningful human creative input.

ApproachRejected in favour ofReason
Header-name column reading in SheetsPositional index readingFragile to client spreadsheet edits
Retry-loop after Sheets writeClient-side WALNon-deterministic propagation delay
Single tenants table in PostgresPer-tenant JSON config fileSimpler ops; no DB needed to boot
Weekly schedules for allocationsProject-lifetime envelopesApples-to-oranges availability comparison
Separate vendors tableemployment_type on usersAvoids dual-schema maintenance
project_users membership tableEnvelope-as-assignment modelEliminates sync between membership and hours
AGPL or MIT open-source licenceProprietary all-rights-reservedSingle-author product, no contributor community

Commit Message Standard (Human Authorship Evidence)

To maintain a clear record of human creative direction, commits that implement AI-assisted code should follow this format:

<verb> <what changed>

Decision: <why this approach was chosen over alternatives>
Rejected: <what was considered and not used>

Example:

route all Sheets writes through DAL dispatcher

Decision: centralising all append/update calls in lib/dal.sheets.js
ensures pg shadow writes fire on every path without per-call changes.
Rejected: adding pg calls individually at each of 47 call sites — too
error-prone and would have missed sites silently.

When a commit is a straightforward mechanical change with no architectural content (e.g. fixing a typo, bumping a version), a plain one-line message is fine.