Questify

2.20.0

Released

Features

  • With NEXT_PUBLIC_FEATURE_DEADLINE_REMINDERS on, whoever is responsible for a task with a due date is emailed as the deadline approaches and again when it passes — by default a week before, a day before, and when the task is overdue. The email gives the deadline as a local date and time in the recipient's timezone, or the quest's, or UTC. A quest owner who is emailed on behalf of an unassigned task, or of a member with no account, is told who the task is for. Party Quest owners get one email per quest listing up to 50 open tasks that are overdue or due within the widest window (a week by default), with who each task is for, on a day a member's task comes due. The profile's Email preferences section gains a Deadline reminder emails toggle and shows timezone and quiet hours when it is on. Each email's unsubscribe link turns off deadline reminders only. (#572)

  • With NEXT_PUBLIC_FEATURE_PARTY_INVITES on, a Party Quest owner invites people by email address, including people with no Questify account. The party panel lists pending invitations separately from joined members, marks expired ones, and can send an invitation again or revoke it. It says when each link was issued rather than sent, since the email may not have gone out. The invited person gets an email linking to the new public /party-invites/<token> page. That page shows who invited them and to which quest, offers sign-in or sign-up, and after sign-in has a Join button that opens the quest. Only the account with the invited address can join. If the email cannot be sent, the owner is shown the link to pass on. Signup now keeps a next destination, including through the confirmation email, and can pre-fill the address, so a new account returns to the invitation instead of going to onboarding. With the flag off, inviting seats an existing account immediately, as before. (#573)

  • Added an admin User Analytics page at /admin/analytics, linked from the admin sidebar and dashboard. It shows account and activity counts, a daily signup chart for 7, 30 or 90 days, onboarding totals and a per-step table, activation and adoption shares, and the plan mix. Each section reports its own read failure instead of showing zeroes. Each account on /admin/users now shows its plan, last activity, last sign-in, onboarding state, quests created and completed, tasks completed, investigations, coach sessions and quest narratives. (#609)

Fixes

  • An investigation that fails because the model spent its whole response budget thinking now says so. DeepSeek V4 thinks before answering by default, counts that thinking against the budget, and returns it as reasoning_content, which the worker did not read. The reported DeepSeek failure therefore said the model had stopped at the limit before answering, with no mention of thinking. Anthropic replies that contain only thinking are now reported the same way. The failure messages shown on the task card now tell the person to ask again or choose another persona, and no longer name an environment variable. The failed row keeps the reply's finish reason, token usage and whether it was only thinking in details.failure, and the worker log explains where the budget is set. The request sent to DeepSeek is unchanged; the 4,800-token budget from 2.19.0 is the fix for the reported failure. (#623)

  • The coach and the quest narrative now log why a DeepSeek or OpenRouter reply came back empty: its finish reason, and whether the model sent only thinking. (#623)

  • The "No activity for N days" banner now disappears as soon as a task on the quest is started, completed, added or investigated. Before, it stayed until the next 09:00 UTC stall check, up to a day after the person had gone back to the quest. (#605)

  • Emails now arrive from "Questify" instead of from the sending address's local part, such as "no-reply". The web app and the worker both send a bare FROM_EMAIL as Questify <address>, and send a value that already has a display name, such as Questify Support <help@questify.co>, unchanged. A worker email whose first attempt reached Resend before this deploy and is retried after it, within Resend's 24-hour idempotency window, can be refused as a changed request and recorded as failed instead of being sent. (#634)

  • A Party Quest owner whose opted-out member had a task coming due no longer gets an outstanding-work digest for it, and an owner is no longer sent a digest that lists only tasks already finished. (#639)

  • A Party Quest with 100 expired invitations no longer refuses new ones until the owner revokes old ones by hand. (#639)

Technical changes· 24 entries for database, tooling, CI, tests and docs

Database

  • Added deadline reminders. A new pg_cron job, enqueue-deadline-reminders (hourly at :20), calls enqueue_deadline_reminders(), which writes a deadline_reminder_deliveries row for every open task on an active quest whose current deadline window opened within the last day. The windows live in the new service-role-only deadline_reminder_windows table, seeded with a week, a day and overdue, and only a task's current window is reminded. The selection query is deadline_reminder_candidates(). The reminder goes to the task's assignee while their party seat still grants access, and otherwise to the quest owner (task_deadline_recipient()). A partial unique index allows one send per task, window, deadline and recipient, so repeated runs send nothing new, while a moved deadline or a reassigned task is reminded again. When a run reminds someone other than the owner, the owner also gets an outstanding-work digest for that quest, at most once per quest per UTC day, and the owner's own reminders for that quest from the same run are folded into it when it lists them. The digest lists at most 50 tasks, and a reminder for a task beyond that is still sent on its own. claim_deadline_reminder_delivery(), complete_deadline_reminder_delivery() and fail_deadline_reminder_delivery() let the worker send rows. A claim skips a row whose task or quest changed, whose task has moved into a later window, whose recipient changed, or whose address or opt-out no longer allows it. Quiet hours delay a row rather than skip it, and a row expires 23 hours after it was written. notification_preferences gained deadline_reminder_emails (on by default), accepted by update_my_notification_preferences() and by the one-click deadline_reminders unsubscribe category. Every new function is service-role only. (#572)

  • Party Quest invitations can now be pending. quest_party_invitations gained invitee_email, token_hash, expires_at, last_sent_at, send_count, revoked_at and revoked_by_id, and its status now defaults to pending. Four new functions do all the writing. create_party_invitation() lets a quest owner invite an address to a quest that has not been deleted, or send an existing pending invitation again, and returns a single-use token once. Only the token's SHA-256 digest is stored, and each token expires after 14 days. revoke_party_invitation() withdraws a pending invitation and keeps its row. get_party_invitation_preview() can be called signed out and describes a token. accept_party_invitation() creates the member seat only when the token is live and the caller's confirmed email matches the invited address. Issuing is rate limited: 50 tokens per inviter in 24 hours, recorded in the new client-invisible quest_party_invitation_sends table, whose rows outlive the invitation they count, a 10-minute wait before re-sending, and 100 open invitations per quest. Owners lost their direct UPDATE and DELETE policies on invitations. Their INSERT policy now admits only the legacy directly-added audit row. An invitee can read their own pending row by confirmed email, through the new current_user_confirmed_email(). Existing pending rows, which had no token and could never be accepted, are marked revoked. (#573)

  • Added admin-only user analytics functions. admin_user_analytics_summary() returns headline counts: accounts, new accounts over 7 and 30 days, accounts active in the last day, 7 days and 30 days, accounts that never signed in, onboarding eligibility, starts and finishes, and how many accounts have ever created a quest, completed a task or quest, used AI, played a shared Party Quest or joined a guild. admin_user_signups() returns new accounts per UTC day over 7 to 90 days. admin_onboarding_funnel() returns completed, skipped and stopped counts for each wizard step. admin_plan_mix() returns accounts by plan tier and who set it. admin_user_activity() returns plan, last sign-in, last activity, onboarding state, and quest, task, investigation and coach counts for up to 100 given accounts. Each is security definer, refuses callers is_admin() rejects, and is revoked from anon. Their three helpers are not callable by clients. A new trigger, task_investigations_stamp_initiator, now records the signed-in caller as an investigation's initiated_by_id and stops signed-in updates from changing it. Before this, a party member could name another account. An investigation is credited to the account its first charge debited, then to a recorded initiator who owns the quest or holds a seat on it, and otherwise to the quest owner. "Used AI" also counts quest narratives, which leave no ledger row on a free model. Last activity is the later of the last sign-in and the newest session refresh. The public quest catalog's system account is excluded. Accounts backfilled before the onboarding wizard existed are left out of every onboarding figure. (#609)

  • A stall notification now ends as soon as its quest sees activity, instead of at the next daily check_stalled_quests() run. New statement-level triggers on tasks (insert and update) and task_investigations (insert) delete the quest's notification and log the same resolved event, with reason new_activity_detected, that the daily run writes. Activity is defined as the run defines it: a task's updated_at or an investigation's created_at inside the notification's threshold. A trigger on quests does the same, with reason quest_no_longer_active, when a quest leaves active. The new stall_quest_has_recent_activity() answers whether the next run would remove a quest's notification, and every reader of a notification now uses it. get_active_stall_notifications() also requires the quest to still be active. enqueue_stall_email_digests() leaves such a notification out of the digest, and claim_stall_email_batch() skips a digest in which every stall has seen activity. This covers a task write that commits while a run is in progress, which leaves a notification in place until the next day's run. stall_quest_has_recent_activity() runs as the caller, is callable by signed-in users but not anonymously, and was added to the generated types. The three trigger functions are not callable by clients. (#605)

  • Three fixes from the release review, in 20260923120000_release_2200_review_fixes.sql. enqueue_deadline_reminders() no longer writes an owner digest when the only reminder for someone else's task was written as skipped because they opted out. claim_deadline_reminder_delivery() now skips a digest as nothing_outstanding once none of the tasks it lists is still open and due within the widest window, even while other tasks on the quest are open. create_party_invitation() no longer counts expired pending invitations towards the 100-open-invitations cap. No signature or grant changed. (#639)

Tooling & Configuration

  • Added the NEXT_PUBLIC_FEATURE_DEADLINE_REMINDERS flag (off by default) to .env.example, the web and worker env schemas, the web Dockerfile, docker-compose.yml and the Playwright web server. The worker has a new deadline reminder loop that sends only when that flag and RESEND_API_KEY, FROM_EMAIL and QUESTIFY_SITE_URL are all set. When the flag is on but a value is missing, the worker logs an error at startup naming the missing values. DEADLINE_REMINDER_POLL_INTERVAL_MS (default one minute) sets how often the loop checks for rows to send. (#572)

  • Added the NEXT_PUBLIC_FEATURE_PARTY_INVITES flag (off by default) to .env.example, the web env schema, turbo.json, the web Dockerfile, docker-compose.yml and the Playwright web server. robots.txt now disallows /party-invites/. (#573)

  • The dev TUI's From address field now accepts Name <address> as well as a bare address, and says that a bare address is sent as Questify <address>. .env.example and docs/ops/deployments.md say the same. (#634)

CI/CD

  • The worker deploy now copies NEXT_PUBLIC_FEATURE_DEADLINE_REMINDERS from Vercel production as well, passes it with flyctl deploy --env, and refuses to deploy if a Fly secret of that name would override it. The migrations-only pgTAP step in supabase.yml now runs deadline_reminders.sql. (#572)

  • turbo.json now passes NEXT_PUBLIC_FEATURE_QUEST_RECURRENCE, NEXT_PUBLIC_FEATURE_GUILDS, NEXT_PUBLIC_FEATURE_ATTACHMENTS, NEXT_PUBLIC_FEATURE_STALL_DETECTION, NEXT_PUBLIC_FEATURE_EVENT_REMINDERS and NEXT_PUBLIC_FEATURE_DEADLINE_REMINDERS through to test:e2e. Turbo's strict env mode had stripped them, so the Playwright web server always ran with those flags off whatever CI set. (#639)

Tests

  • Added supabase/tests/deadline_reminders.sql (66 assertions). It covers the schedule, the privileges, the window configuration, the selection query, recipient fallback, the one-send-per-window dedupe, the owner digest and its 50-task cap, every claim skip reason, quiet-hour deferral, expiry, backoff and the new preference. rls.sql gained 10 assertions for the new tables, functions and preference column. The worker has renderer, delivery and loop tests, and the web app has tests for the new category and flag in the preference form, its parser and action, the unsubscribe page and route, and the Playwright notification preference spec. worker-deploy.test.mjs checks the third worker email flag. (#572)

  • Added supabase/tests/party_invitations.sql (62 assertions). It covers privileges, issuing and re-sending, direct-write and direct-delete refusal, a ledger that survives deletion, the preview, who can read an invitation, address-bound and single-use acceptance, revoking, expiry, both rate limits, and invitations to and from deleted quests. The web app has tests for the invitation queries and email, the three new Server Actions, the party panel's pending list, the landing page and its join form, and signup's next handling. The new e2e/party-invitations.spec.ts covers the landing page in both flag states and the robots rule, and in live runs the owner's invite-and-revoke flow and an invitee joining from the link. (#573)

  • The worker has tests for DeepSeek reasoning_content, Anthropic replies that contain only thinking, failure messages that name no environment variables, the operator log line, and details.failure on the failed row. The web app has tests for the empty-reply log lines. (#623)

  • Added supabase/tests/admin_user_analytics.sql (46 assertions). It covers refusing anonymous and non-admin callers, excluding the system account, activity from a session refresh, the pre-wizard onboarding backfill, crediting a party member's task to that member, the investigation initiator stamp, crediting investigations by payer or a verified initiator, zero-cost narratives, signup window clamping and UTC days, and the 100-account cap. rls.sql gained 2 privilege assertions. Its account-deletion test now clears the fixture investigation's initiator first, because Postgres re-checks foreign keys on a row updated in the same transaction that inserted it. The web app has tests for the analytics readers and formatting, the analytics page and the users page's per-account figures. The new live-only e2e/user-analytics.spec.ts checks the page, its signup windows, the users list and non-admin refusal. (#609)

  • Added supabase/tests/stall_resolution.sql (25 assertions). It covers resolution by starting, adding and investigating a task and by completing the quest, the resolved events and their reasons, edits that are not activity, activity older than the threshold, and agreement with the next daily run. It also checks that a notification written by a run that raced a task write is hidden from the banner, left out of the email digest, and removed by the next run, that a queued digest is skipped once all its quests have seen activity, and the new functions' privileges. (#605)

  • Added formatSender() tests in both apps, and Resend request tests that pin the named sender in the web app and the worker. A dev TUI test fails if either app's bare-address pattern stops matching the TUI's own From address check, so an address the TUI rejects, such as hello@questify, is never renamed. (#634)

  • deadline_reminders.sql covers an opted-out assignee's task not producing a digest, and a digest whose listed tasks are done being skipped while unlisted tasks stay open. party_invitations.sql covers an expired invitation freeing a place under the cap. A dev TUI test fails when playwright.config.ts reads a variable that turbo's test:e2e task does not pass through. (#639)

Documentation

  • docs/ops/scheduled-jobs.md describes the deadline reminder job, how to change its windows, and how to check it and stop it in production. docs/notifications.md and docs/ops/deployments.md list the new category and flag. (#572)

  • docs/notifications.md describes Party Quest invitations: why their email bypasses the preference path, what each database function decides, and the landing page. docs/ops/deployments.md names the new flag. (#573)

  • docs/ai-investigations.md explains the #623 failure, where an empty answer's details are recorded, and that DeepSeek V4 counts its thinking against max_tokens. (#623)

  • Added docs/admin-user-analytics.md, which defines every user analytics metric and states what it cannot show. (#609)

  • docs/ops/scheduled-jobs.md says that the daily run detects stalls, and that activity on the quest ends a stall at once. (#605)

  • docs/notifications.md has a new Sender name and logo section: how the From header is built, where the Supabase Auth sender name is set, and the DMARC, logo, certificate and DNS steps Gmail needs before it shows a logo beside the sender. (#634)

  • AGENTS.md and docs/notifications.md say that a NEXT_PUBLIC_FEATURE_* flag is not enforced by the database, and why the invitation functions stay callable with the flag off. docs/ops/scheduled-jobs.md and docs/notifications.md describe the review fixes above. (#639)