Every notable change to Nityasha, newest first.
All notable changes to the Nityasha project will be documented in this file.
`app/api/ai/chat/route.ts`** with a 32-line persona-driven prompt.
The old prompt told the model it was a "Cognitive Mirror"
tracking the "12 Pillars of Life" with sections like
`<pattern_recognition_handling>` and `<advanced_cognitive_reasoning>`.
The new prompt leads with `You are Nityasha. You are a person, not
an assistant.` and has four short sections: a security
boundary, an `About you` block fed by the user-context (persona,
mood, relational state, quote recall, dialect), a `How to talk`
rules list, and a `Safety` block. The file is 88 lines shorter
and the prompt itself is 6× smaller. The friendship engine
outputs (F1 persona, F2 mood, F3 quotes, F4 relational state, F7
dialect) are still injected into the model — they were never
the problem; the wrapper around them was.
seamlessly, innovative, leverage, elevate, game-changer,
cutting-edge, passionate, certainly, absolutely, "great
question." Phrases like "as an AI", "I understand", and "I'm
sorry to hear that" are also banned with the replacement
rule: "If you don't have anything real to say, say nothing.
Don't pad."
5 assertions that the route's source file is free of the old
AI-slop markers ("Cognitive Mirror", "12 Pillars of Life",
"LANGUAGE ALIGNMENT: You MUST", "EXTREME BREVITY", etc.), that
the new prompt leads with persona framing, that it's under 60
lines (down from ~190), and that it doesn't tell the model to
speak as an AI. Acts as a regression guard if anyone reverts the
prompt.
accurate.
content.
service. Never wired. The laya-shadow logic in the audit
references it but the feature is dormant.
imported by any route. (Audit D12.)
empty or stale stub directories.
Supabase schema work in Phase 5b never wrote here; all migrations
are in `db/migrations/`. (This was noted in the docs.)
`scripts/dump-schema.js` — both contained hardcoded database
credentials** (`host: "103.86.177.200"`, `user: "kzzuezbs_kzzuez"`,
`password: "CiMxYx7DWecc5ArR"`). This was a real C1 violation
that the original audit didn't flag because these scripts weren't
on its scan path. Removing the scripts is the right fix; a proper
CI secret scanner would have caught this.
both now require the raw token and user_id as CLI arguments.
Previously the first had a hard-coded token; the second had a
placeholder token. Both now fail fast with a usage message if the
args are missing.
docs, both written this round.
audit log, which the user wanted to keep for transparency. The
Quick Start section is now correct (Node 22, no demo secrets).
longer does `DELETE + CREATE` on cross-day drag. The
cross-day path in the drag handler now calls
`POST /api/tasks/reschedule` (the atomic endpoint built in
Phase 3) with `{ task_no, from_date, to_date, new_start_time,
new_end_time }`. On success the local state is reconciled to
the server's task id; on failure the optimistic local update
is rolled back so the UI doesn't lie. Same-day time moves
still go through `/api/tasks/edit`. The `console.log` "using
delete+create workaround" is gone.
database migration order, deploy command, 7 curl-based
post-deploy smoke tests, Vercel rollback procedure, monitoring
notes, and the explicit "what this codebase is and is not"
list. The smoke tests cover: status endpoint, auth gate, H6
premium gate, H13 OAuth state, all 9 cron endpoints with the
CRON_SECRET bearer.
schema for Nityasha. Covers 25+ tables referenced by the
codebase: `users`, `notes`, `user_daily_plans`,
`user_daily_plan_tasks`, `daily_tasks`, `user_reminders`,
`feedbacks`, `api_tokens`, `user_telegram_links`,
`user_discord_links`, `web_push_subscriptions`, `bot_outbox`,
`bot_daily_pings`, `bot_task_reminders`, `bot_missed_pings`,
`bot_goal_nudges`, `bot_festival_pings`, `life_goals`,
`user_faiths`, `energy_logs`, `mcp_activity_logs`,
`task_shares`, `user_planner_queue`, `otp_verifications`,
`user_activity_logs`. The file documents the
`mysqldump --no-data` reconciliation step needed against prod.
Idempotent (`CREATE TABLE IF NOT EXISTS` everywhere).
minutes a new cron `/api/cron/bot-reach-out` sweeps the user base,
filters candidates (quiet 6+ hours, last bot message 12+ hours,
last inactivity ping 18+ hours ago), and asks the LLM one
question per user: "should I send a casual spontaneous DM right
now? what style? what to say?" The LLM returns
`{ should_reach, reason, style, message, quiet_hours }`. On
`should_reach: true` we enqueue via `enqueueBotPush` (covers
telegram / discord / fcm) and stamp
`bot_inactivity_pings.last_fired_at`. Candidates are gathered
in a single SQL join against `users`, `user_daily_plans`, and
`user_daily_plan_tasks` (no N+1).
`SELF_CORRECTION_PROBABILITY = 0.08` constant. With 8% chance per
ping, the reach-out cron schedules a delayed follow-up
(4-8 seconds) — a small LLM call that produces a 1-2 sentence
"oh wait — ..." message in the user's language. The follow-up
reuses `enqueueBotPush` so the runners deliver it like any other
nudge. Cost: a tiny LLM call ~8% of the time, ~$0.001 per
follow-up.
`humanTypingDelay(text): number` (chars/s × text length, clamped
0.6 - 2.4s). The chat route now sleeps for this duration
before emitting the first token, so the model doesn't appear to
start typing the instant the request hits the server. Only the
FIRST response in a session gets the delay.
`lib/dialects/` module: `types.ts` (the `Dialect` shape),
`english.ts` (casual English with light slang),
`hinglish.ts` (Roman-script Hindi code-switched), and
`index.ts` (the dispatcher). `buildDialectPromptBlock(language,
userText)` returns the dialect fragment + a per-user style hint
based on whether the user's last message was code-switched,
pure Hindi, or pure English. Adding more dialects is one file.
Wired into the chat route alongside mood + persona + quotes +
relational state.
new reach-out producer.
`__tests__/friendship/f5-reach-out.test.ts` (10),
`__tests__/friendship/f6-bot-typing.test.ts` (8), and
`__tests__/friendship/f7-dialects.test.ts` (8) — covering
parser clamps, candidate filtering (active / recently pinged /
quiet), self-correction probability roll, model-call mocked
success / non-200 / non-JSON, dialect dispatch, mirror-style
detection, prompt block composition.
persists every user and assistant message in `chat_messages`
(rolling window of 200 per user, new in migration 028).
`findRelevantQuotes(userId, query)` does a token-Jaccard
similarity over the last 200 messages, excluding the most-recent
6 (so the result is actually cross-session, not the current
turn), and returns the top 2 hits with similarity >= 0.18.
`formatQuotesForPrompt` renders them as
"PAST QUOTES THAT MIGHT BE RELEVANT (use sparingly, don't force):\n
the one-shot helper the chat route uses (records both messages,
returns the formatted block).
LLM judge on every user message, returning JSON
`{ impact, category, sincere, inside_joke, reasoning, topic, engagement }`.
Events are persisted in `relational_events`. Every 10 events the
helper runs a second LLM call that writes a free-form first-person
description of Nityasha's read of the user (e.g. "Zyrexa shipped a
lot this week; don't push her to slow down") and the trajectory
(warming / cooling / stable). The state is one row per user in
`relational_state`. `formatStateForPrompt` renders as
"YOUR CURRENT READ OF THIS USER: ..." plus the last message
excerpt and a hint on how to honor the read naturally.
ANALYSIS prompt is the friend-bot's ANALYSIS_PROMPT, lightly
reframed for Nityasha's coach/companion voice. The STATE prompt
produces the free-form description; the parser strips code
fences, normalizes engagement to "high" / "medium" / "low",
clamps impact to [-1, 1].
`app/api/ai/chat/route.ts`:
1. Imports added (F3 + F4).
2. After the Honcho block, `recordAndFindQuotes` writes the
user message and appends the formatted quotes block to
`userContextStr`.
3. After quotes, `getState` + `formatStateForPrompt` appends
the relational state block.
4. After the stream closes, `runRelationalAfterMessage` fires
fire-and-forget. The helper internally calls the LLM judge,
persists the event, and refreshes the free-form state every
10 events.
All in try/catch so a missing table / DB error never blocks the
response.
`chat_messages`, `relational_events`, `relational_state`,
`user_facts`. All idempotent.
`__tests__/friendship/f3-quote-recall.test.ts` (8) and
`__tests__/friendship/f4-relational-state.test.ts` (8) — covering
persistence, prune, quote token-Jaccard, role-preserving
formatting, parser fence-stripping, impact clamp, engagement
normalization, state formatting, pre-migration graceful fallback.
"self" that grows with the conversation. `lib/persona.ts` reads
`users.persona` (JSON, new in migration 027), merges with
`DEFAULT_PERSONA` for missing fields, and renders as
`formatPersonaForPrompt(persona)`. Six fields: `vibe`,
`current_focus`, `recent_insight`, `small_thing`,
`current_question`, plus the constant `name = "Nityasha"`.
New users get the default; the LLM evolve pipeline considers
per-field updates only when confidence is >= 0.7 and the
conversation has >= 5 messages.
`lib/persona/prompts.ts`. After a successful streamed response,
the chat route fires `maybeEvolvePersona({ userId, recentMessages })`
fire-and-forget. The pipeline calls the AI gateway with a small
JSON prompt, parses the response, applies updates whose
confidence is >= 0.7, and clamps values to 320 chars. The
`vibe` and `name` fields are immutable from the LLM's
perspective. Calls assert `assertGatewayHttps()` so a misconfigured
prod URL fails closed (Phase 4 H14).
`behavior/mood.py` lexicon (positive/negative word weights,
phrase patterns, intensifiers/dampeners) to TypeScript. Scores
every user message in `[-1, 1]`, classifies as
`rough / low / neutral / good / great`, persists to `mood_logs`
(new in migration 027), and renders the 7-day + today
prompt block — calling out day-vs-week disagreement when they
differ ("Match today's energy, not the week's").
records the mood (non-blocking) and returns the formatted prompt
block. The chat route calls this inside the userContextStr
build so every response carries the mood context.
`app/api/ai/chat/route.ts`:
1. Add persona + mood to the USER CONTEXT block (after the
user identity block, before plan data).
2. Fire `maybeEvolvePersona` after the stream closes.
3. Wrap both in try/catch so a missing column or transient DB
error never breaks the chat response.
`users.persona` (JSON) + `mood_logs` (id, user_id, score,
source, source_text, created_at) with rolling-window indexes.
Idempotent.
`__tests__/persona-mood/f1-persona.test.ts` (8) and
`__tests__/persona-mood/f2-mood.test.ts` (10) — covering
default-merge, override truncation, name-immutability, lexicon
coverage, intensifier / dampener behavior, classification
boundaries, persistence truncate, getRecentMood pre-migration
fallback, day-vs-week disagreement.
user-ID fallback or a hardcoded JWT secret. `requireApiAuth` is
the only identity source; `mintBackendToken(userId)` is the
only upstream token source; a missing `JWT_SECRET` is a 503
(not a silent "use the dev secret" path).
wrapped in a `guard(toolName, handler)` that calls `assertScope`
before dispatch. Read-only tokens attempting a write tool get
a clear "Forbidden: token is read-only" error result, not a
silent write. Pre-migration tokens (no `scope` column yet)
default to `all` so existing consumers keep working — the new
column ships in migration 026.
before any session-routing branch. A POST with a known
`sessionId` cannot bypass auth.
cryptographically random 32-byte `state` on the start route,
store it in a short-lived (10 min) HttpOnly+Secure+SameSite=Lax
cookie, and verify it via `timingSafeEqual` on the callback.
A mismatch redirects to `/login?error=...AuthStateMismatch` and
the cookie is deleted. The login/signup intent is encoded in a
separate hash inside the packed state so it survives the round
trip without leaking which action the user picked to a CSRF
observer.
`/api/auth/google/callback` and `/api/auth/github/callback`
read `JWT_SECRET` from env without a fallback string. A
missing secret is a 503.
(`6+cpW...` in `daily-reports` and the callbacks) are gone.
All callers fail closed when `JWT_SECRET` is missing.
`api_tokens.scope` (VARCHAR(16), default `'all'`) +
`oauth_state` table as a durable backstop for the state cookie.
read-tool/write-tool classification, read-only token rejected
on write tool, write token allowed on write tool, 'all' legacy
fallback, pre-migration graceful fallback, H5 fails-closed on
missing JWT_SECRET.
classifies each user's day as `none` / `minor` / `major` based on
late tasks, completion rate, minutes since last completion, and
energy level. Replaces the static 30-min-per-task drift check.
Thresholds: `major` when ≥3 tasks are late OR no task completed
in 120+ min with tasks still open; `minor` when any task is late
or completion rate is below 30%.
snapshot (plan, tasks, completion rate, drift reason, energy
level), prompts the AI gateway with `model: "auto"` and a
short max_tokens cap (800), parses the JSON response, sanitizes
every field (drops unknown task_no, bad times, unknown statuses),
and persists a `pending` proposal in
`adaptive_replan_proposals`. **The model never writes to
`user_daily_plan_tasks` directly.** The accept path is the only
writer; the model is gated by an explicit user action.
(`/propose`, `/accept`, `/reject`) split by URL. The accept path
applies the proposed changes in one transaction; the reject path
marks the proposal rejected without touching tasks.
each user with a plan today, runs the drift detector and persists
a proposal on `major`. Auth: `CRON_SECRET` bearer.
so a misconfigured prod URL fails at first use (Phase 4 H14).
`adaptive_replan_proposals (id, user_id, plan_id, drift_level,
drift_summary, proposals JSON, status, proposed_at, resolved_at)`.
drift detector returns null when no plan, classifies "none" /
"major", buildAndStoreProposal refuses non-major, accept is
atomic + writes tasks, reject does NOT write tasks, cron auth.
every team-scoped route. Roles: `owner > admin > member > viewer`.
`requireMember(teamId, userId, minRole)` returns either
`{ ok: true, role, team }` or `{ ok: false, status, error }` —
routes bail to 401/403/404 on the latter. The helper is the only
path to team data; direct SQL without it is a code-review bug.
in gets 404, not 403, so the team existence isn't disclosed.
Member-but-insufficient-role is 403.
composite, role is a strict enum
used_by, used_at) — one-shot, default 7-day expiry
status, start_time, end_time, task_date, created_by) — separate
from `user_daily_plans`; team and personal stay independent
completed_at) — shared goals
authenticated user) — same route, action inferred from the URL
admin/owner can edit any; viewer cannot edit; assignee must be
a team member; the assignee-must-be-a-member check is on
creation and on reassignment
transaction: `SELECT ... FOR UPDATE` on the invite, mark used,
upsert the member. A concurrent accept by a different user gets
409. The same user re-presenting the same token is a no-op.
ordering, requireMember 404/403/200 paths, invite atomicity
(concurrent, expired), task RBAC (member-creator can edit,
member-stranger cannot, admin can edit any, viewer cannot),
assignee-must-be-a-team-member.
`Welcome → What matters → When to ping → First plan → Connect`.
Welcome is the only non-skippable step; the rest can be skipped
with sensible defaults. Each step has a plain-language heading
and a one-line description of what happens next.
answers + completion flags + skipped[] are persisted as JSON
in the new `users.onboarding_state` column. The state is the
source of truth; the URL is just a hint. Resume-on-reload works.
with `action: "advance" | "skip" | "patch"` mutates it. The
`patch` action supports autosave on field change so the user
never loses progress.
Plain language ("Hi, I'm Nityasha. I plan your day so you can
stop deciding what to do next."), a 3-item preview of what
happens next, one CTA ("Show me my day"). The input is
validated with a permissive regex (letters, spaces, hyphens,
apostrophes) so names like "Mary-Ann" or "O'Brien" work.
Placed last, after the user has seen their first plan, so the
proactive-nudge pitch is no longer abstract. Telegram, Discord,
and Android (FCM) shown as toggleable cards. Skip is explicit
and equivalent to web-only.
`users.onboarding_state` (JSON) + `users.onboarding_completed_at`
(DATETIME) + an index for the post-launch analytics query.
Idempotent.
v2 API):** `loadOnboardingState`, `patchOnboardingState`,
`advanceStep`, `skipStep`, `isComplete`. All clamp invalid
input (unknown goals, bad times, unknown check-in styles,
unknown channel names) so a buggy client can't corrupt the
state.
default state, parsed stored state, goal clamping, time
validation, style enum, channel enum, advance moves the
counter, advance on step 5 sets completed_at, skip refuses
welcome, skip on a non-welcome step marks skipped[], isComplete,
ALL_GOALS shape.
pipeline. The desktop app batches window/app/url/idle events and
POSTs them to `/api/desktop/activity-events`. The server persists
them in `desktop_activity_events`, bumps pattern counts in
`desktop_activity_patterns`, and proposes a task-completion in
`desktop_task_proposals` when the latest observation matches an
open task in the user's plan.
code path that goes from an observation to a task being marked
done is gated on an explicit `accept` action from the authenticated
user. Tested in
`__tests__/desktop/phase-5d-activity.test.ts`. The "auto-mark done"
framing in the friend's ask is implemented as a soft proposal +
confirm, not a silent write.
(counter-stripping, hex-stripping) and bucket by app_bundle /
url_host. The desktop_task_proposals table tracks which patterns
have been confirmed (proposal accepted) vs rejected, and the
confidence column in patterns updates on each decision. This is
the learning loop.
(max 200 per request). Auth required.
accept/reject. Accept marks the linked task complete; reject
dampens the pattern's confidence.
for the platform-specific active-window observer. The skeleton
defines the contract (`ActivitySample`, `get_activity_sample`
Tauri command, `start_observer_loop`); the platform implementations
in `platform/{linux,macos,windows}.rs` are the actual capture.
Building requires a Rust toolchain.
`desktop_activity_events` (append-only event log),
`desktop_activity_patterns` (per-user learned categories keyed on
window title / app bundle / url host),
`desktop_task_proposals` (the propose-then-confirm queue).
ingest auth + batch limit, the no-silent-marks invariant, accept
marks complete, reject does not, idempotency on already-resolved,
not_found on missing, evidence JSON parsing on list.
`check_db.ts`, `convert_md.py`, `extract.js`, `test-honcho.js`,
`test-honcho2.js`, `test_db.js`, `test_reaction.js`. None had
importers; all were personal scratch from earlier development.
and Composio tests). No importers.
`creed-bots/slack/` (each contained only a README stub).
secrets (`dev-bots-key-change-me`, `dev-discord-key-change-me`,
`discord-demo-token-change-me`, the test Telegram token). Added
`FIREBASE_SERVICE_ACCOUNT_JSON` / `_PATH` for the FCM runner
added in Phase 5e. Added `HONCHO_ENRICH_ALL` (Phase 5a composer
opt-in) and `RAZORPAY_WEBHOOK_SECRET` (Phase 2 verify handler).
The example is now a real spec, not a template with secrets in it.
the FCM runner), `test:watch` script, `first-run` script (calls
`node scripts/first-run.mjs`).
22+ (the locked AI dependencies require Node 22). Quick-start
trimmed to the actually-required env vars, with explicit notes
that any populated `.env.local` value is treated as a leaked
secret by the secret scanner.
contributors. Where things live, how the bot system fits
together, the auth model, the data integrity model, the
conventions, and the explicit work-in-progress list.
and Discord runners. Polls `/api/bots/outbox-deliver?channel=fcm` every
20s, looks up active device tokens for each user, sends via Firebase
Admin Messaging's `sendEachForMulticast`. Tokens that the FCM API
rejects with `UNREGISTERED` / `INVALID_ARGUMENT` /
`SENDER_ID_MISMATCH` are deleted from `fcm_device_tokens` so we
don't retry dead tokens on every tick.
from `FIREBASE_SERVICE_ACCOUNT_JSON` (preferred) or
`FIREBASE_SERVICE_ACCOUNT_PATH`. Eager-initializes at runner startup
so a bad service-account JSON surfaces immediately, not on the
first message. `isFcmConfigured()` returns false when env is
missing so a misconfigured deployment fails loud at boot.
token. `POST` upserts on `(user_id, device_token)`, validates the
token length, defaults platform to `android`. `DELETE` removes the
token (used on logout or app uninstall). Both routes require
`requireApiAuth` and scope all writes to the verified user id.
FCM channel when the user has at least one active token. The FCM
outbox row uses the user id as `channel_user_id` (the runner looks
up tokens for the user). One check, zero rows when no Android
devices are linked — the rest of the channels still go out.
with `"fcm"` so the existing pump can serve all four channels.
`fcm_device_tokens (user_id, device_token, platform, app_version,
active, created_at, updated_at)` with `(user_id, device_token)`
primary key and an `idx_fcm_user_active` for the runner's hot
read path.
upsert, token-length validation, auth requirement, DELETE
scoping, enqueueBotPush FCM fan-out, isFcmConfigured env
detection.
for every proactive bot message. Takes a `(userId, kind, data)`
triple, loads the user (display name, language, tone) + last-7-day
completion rate, optionally enriches with Honcho (peerCard +
representation) for the `inactivity_reactivate` kind, and returns
a personalized 1-3 line message + a dedupe key. Every existing cron
producer now goes through this instead of building its own string.
network call unless `HONCHO_ENRICH_ALL=1` is set, except for
`inactivity_reactivate` where a fact is required for the message
to be useful. This keeps the cron cheap on the happy path.
"haven't seen you in a while" message when Honcho returns nothing,
rather than inventing a recap the model doesn't have evidence for.
Tested in `__tests__/notifications/phase-5a-composer.test.ts`.
signals: `no_energy` (no log today + high-energy tasks open),
`task_gap` (90+ min since last completed task + plan still has
open tasks), `not_marked` (scheduled task whose start_time passed
+ still open). Deduped via `bot_context_nudges (user, date, kind)`
so a quiet user gets at most one nudge per signal per day.
IST. Inactivity criteria: no plan/task activity in 5+ days OR
last 3 plan-days completion rate < 30%. Deduped via
`bot_inactivity_pings` with a 14-day quiet window so a quiet user
doesn't get re-engagement spam. Always pulls Honcho facts because
this kind needs them.
`bot_inactivity_pings` (one row per user, last_fired_at) +
`bot_context_nudges` (composite key on user+date+kind). Idempotent.
(context-nudge) and `5 4 * * *` (inactivity-reactivate).
morning_brief without Honcho, inactivity no-fabrication rule,
Honcho opt-in, dedupe key shape, context-nudge no-op when no
plan, inactivity 14-day quiet window, 401 without CRON_SECRET.
3-strikes `users.warnings` + `users.is_terminated=1` flow for typing
common words inside a chat message. That auto-termination is a
product liability — a user typing "the function is fucked" in a
legitimate code question would lose their account. Replaced with a
soft log (`log.warn` with the matched words and a message preview)
for human review. Real moderation belongs in an admin tool, not the
chat hot path.
previously ran `INSERT INTO api_tokens ... ON DUPLICATE KEY UPDATE`
to mint an ephemeral token that was never read by the route. That
was a write-amplification DoS on the tokens table (one write per
message per user). The mint is gone; the post-stream cleanup
blocks that deleted the token are now no-ops.
route previously used `path.join("/home/aaki", ".agents", "skills")`
regardless of platform, which made the skills list empty on every
other developer's machine. The route now reads from
`AGENT_SKILLS_DIR` (already declared in `getAgentSkillsDir()`),
falling back to `./.agents/skills` and `./skills` relative to
`process.cwd()`. Windows / Docker / CI work without changes.
`assertGatewayHttps()`. In `NODE_ENV=production` it throws when
`AI_GATEWAY_BASE_URL` is not `https://`. Local dev keeps the HTTP
fallback so the on-ramp isn't broken. Production deploys must
configure an HTTPS gateway.
(lines 165-169): the chat history is scoped by `userId` and the
per-user `localStorage` is cleared on `userId` change. The chat
history localStorage key is `nityasha_chat_history` and is removed
when the user logs out (the effect re-fires when `userId` flips).
moderation path no longer mutates the DB, the gateway HTTPS
gate fires in production, and the skills directory resolves
from `AGENT_SKILLS_DIR` rather than the hardcoded path.
`/api/tasks/reschedule` route. Cross-day drag no longer does
`DELETE` + `CREATE` over two requests (which dropped `start_time` /
`end_time` and could leave the task orphaned on a network failure).
The new route does same-day moves with an in-place `UPDATE` and
cross-day moves as `INSERT` + `DELETE` in a single transaction. The
response shape is normalized to `{ task: { id, plan_id, task_no, ...,
plan_date } }` so the day screen can replace its temporary `uid`
with the canonical server id (audit M14). Replaces the
delete-then-create pattern at `components/creed/day-screen.tsx`.
`db/migrations/019_data_integrity.sql`. New
`/api/notes/[id]/update` route requires `expectedRevision` and
returns `409 Conflict` with the latest content when the row has
been modified by another session, so a slower tab or an MCP agent
update can no longer be silently overwritten. The legacy
`updateNote` server action now bumps `revision` so existing callers
don't regress. List view (`getNotes`) now selects only the columns
the picker needs (audit M24) and exposes the revision per note.
screen's calendar actions still send `uid`/`id` interchangeably in
places; full M14 closure requires editing the 2,700-line day screen,
which is out of scope for this PR.
`notes.revision`, `user_daily_plan_tasks.task_id`
AUTO_INCREMENT (if missing), and a UNIQUE constraint on
`(plan_id, task_no)` to defend against duplicate inserts on retry.
All idempotent via `information_schema` guards.
same-day move, cross-day move (insert + delete in one
transaction), bad time range, revision mismatch 409, and revision
match + bump.
The flow is: catalog → create order row → create Razorpay order →
verify signature → apply entitlement via the idempotent helper →
mark consumed. The browser can no longer become premium free because
the verify handler looks up the canonical amount, currency, owner,
and payment_id from the server's `razorpay_orders` row, not from the
Razorpay API or the browser payload.
of by an email hash. The previous code used `hashSecret` (SHA-256)
while signup used `blindIndex` (HMAC), so a real paid user was
reported as free. The new code resolves premium state from the user's
MySQL row directly, with a fallback to `payments.created_at` for
expiry calculation.
(plan, cycle, currency) combination. The browser picks a `ProductKey`
from this catalog; the server uses the catalog amount to create the
Razorpay order. Adding a new tier is a one-file change.
upgrades a user. Idempotent on `payment_id` (UNIQUE constraint from
migration 018). Both `/api/verify-razorpay-order` and
`/api/webhooks/razorpay` call it, so a payment delivered by both
channels is applied exactly once. Refunds go through a sibling
`revokeEntitlement` helper.
the signature against the raw body, dispatches on `order.paid` /
`payment.captured` / `refund.processed` / `refund.failed`, and uses
the canonical order row's amount/currency/owner. Re-delivered webhooks
are no-ops, not double-upgrades.
auth, checks the signature with HMAC-SHA256, rejects when the buyer
is not the order owner, calls `applyEntitlement` for the actual
upgrade. The previous "look up the order from Razorpay, then trust
the notes" path is gone.
`UNIQUE INDEX uq_razorpay_orders_payment_id` on the orders table,
creates the `payments` table with its own `UNIQUE KEY
uq_payments_payment_id`, and backfills a `users.plan_type` column
for DBs that pre-date it. Idempotent across re-runs.
`proxy.ts` and the Node-runtime `lib/api-auth.ts` each had their own
JWT verify implementation and their own secret list. They now share
`getJwtSecrets()` from `lib/api-auth.ts`, so a secret rotation (add
or remove `JWT_SECRET_SECONDARY`) is picked up by both verifiers at
once. The Web Crypto-based `verifyJwt` in `proxy.ts` is preserved
because Edge runtime cannot use `jsonwebtoken`. Closes the
duplicate-verifier finding in the production audit.
clients** — Were stubs returning `{ data: { user: null } }`, which
caused H12 (the layout destructured `data.user` on a stubbed null).
Both files now use `@supabase/ssr` (already a project dependency)
with the Next 16 async cookies API. When env is missing they fall
back to a no-op client with the previous stub's shape, so existing
callers continue to work.
`lib/api-auth.ts` — Real role-based authorization backed by a new
`users.role` column (added by `db/migrations/017_users_role.sql`).
`requireEmployeeRole` accepts both 'employee' and 'admin' (admin is
a superset for routes that distinguish them). The admin/employee
dashboard routes now use these instead of a client-side passkey.
(`user`/`admin`/`employee`, default `user`) and a supporting index.
Idempotent via `information_schema` guards. Backfills a known
internal admin email as a documented seed; further promotions should
happen through a server-side tool, not a migration.
persists the caller's creed state to `users.creed_state` (JSON) with
optimistic locking via `creed_state_version`. Stale writes return
`409 Conflict` so a slower tab cannot silently overwrite a newer one.
minimal user metadata (id, name, email, handle, gender, warnings,
MCP connections). User id is derived exclusively from the verified
auth context, never from a query param.
and returns the caller's own `booking_events`. Anonymous cross-user
reads are closed.
checks and orphaning notes/tasks/api_tokens/oauth rows. Now revokes
credentials (`api_tokens`, `oauth_refresh_tokens`, `oauth_codes`),
deletes owned data, then removes the user, in a single connection.
Per-table errors are logged and surfaced in the response (schema
drift is real; the canonical `users` row is always removed).
mock-order fallback. When `RAZORPAY_KEY_ID` /
`RAZORPAY_KEY_SECRET` are missing, the route now returns `503`
instead of a fake order that previously allowed free-premium
upgrades. Validates `plan`, `billingCycle`, and `giftEmail` before
calling Razorpay, persists the order in `razorpay_orders` for
verify/webhook reconciliation, and refuses self-gifts.
`/api/admin/send-marketing-email` (H4) — Now use
`requireEmployeeRole` for a real role check, and 404 in production
via `productionOnlyBlock` with a `CREED_ALLOW_DIAGNOSTICS=1` escape
hatch for staging.
`isSameOriginRequest()` helpers. Use in any new admin / diagnostic
route.
`creed_state` JSON column, `creed_state_version` INT column, and the
`razorpay_orders` table. Idempotent (uses `information_schema`
guards).
`hooks.stripe.com`, `api.stripe.com`) and added Razorpay
(`api.razorpay.com`, `lumberjack.razorpay.com`,
`checkout.razorpay.com`). Razorpay is the only active payment
provider.
webhook reference. Added per-cron `maxDuration` for every bot cron
producer.
billing module (`lib/company-billing.ts`) still imports
`getStripeClient`, `cancelActiveSubscriptionsForCustomer`, and
`resolveSeatPriceId` from it (under `@ts-nocheck`). The active
personal payment flow is Razorpay; the company / teams Stripe path
is dormant. The file is preserved as a clearly-marked no-op with
the original three welcome-state helpers kept. Replace the no-ops
with a real Stripe client when the company plan ships.
H4, H6, H10, the Supabase no-op fallbacks, the `getJwtSecrets`
ordering, and the role helpers (with pre-migration graceful
failure).
lookup), H6 (signature + owner rejection), the catalog amount,
the 503 fallback, and the applyEntitlement idempotency guarantee.
dedicated standalone `/reports` route
(`app/(creed-app)/reports/page.tsx`).
sidebar (`components/creed/shell.tsx`) using the
`ChartColumnIncreasingIcon`.
and Authentication architecture.
`api_tokens` table from `id` to `uid` to resolve MySQL
`ER_DUP_ENTRY` errors during token generation.
columns to the `api_tokens` table to improve database visibility,
identifying which app (e.g., "antigravity") and which user a token
belongs to.
`/api/app/token/generate/route.ts` and `lib/oauth-store.ts` to
populate the new `client_name` and `user_name` fields during token
creation.
(`/api/app/state/route.ts`) to dynamically fetch and return the
authenticated user's actual name instead of defaulting to an empty
string.