Canvassing Launch Readiness

Production configuration, native credentials, privacy controls, verification gates, and physical-device tests required before enabling canvassing.

AdvancedownermanageradminUpdated 2026-08-23

Canvassing Launch Readiness

Current V2 release note: V2 uses migration 0068_redundant_spencer_smythe for location-scoped campaigns, GeoJSON polygon territories, assignments, and the append-only foundation audit trail. The property-loading release then applies 0074_sturdy_dazzler followed by 0075_canvassing_scope_retrofit. Together they add private reusable canvassing properties, exact campaign/territory targets, manager map-point and eligible active-client CRM loading, and a bounded target list. 0076_amused_smasher adds strict CSV parsing support plus private staging, caller-bound confirmation, retained confirmation evidence, and bounded cleanup for expired unconfirmed previews. 0078_green_hellfire_club permits invalid source rows to be retained safely for manager correction while keeping ready rows strict, and adds the private actor-bound raw-upload ticket ledger used by app-owned cleanup. The web release adds the private upload, server geocoding/cache orchestration, bounded preview, and explicit confirmation. It does not ship target movement, functional Field view Map/List modes, visits, dispositions, do-not-knock, shifts, GPS, conversion reporting, gamification, AI, or the V1 mobile field runtime described in the complete checklist below. Do not enable or claim those later capabilities based on target storage.

For the V2 foundation, verify migration drift, forced RLS and scoped grants on all four canvassing tables, owner/admin permission backfill, sibling-location and cross-organization denial, stale-revision conflict handling, and campaign, territory, and assignment audit events. The web environment also needs the existing Google Maps browser key for click-to-draw; precise latitude/longitude entry remains available when the map cannot load.

For manager property loading, additionally verify forced RLS and trusted-writer guards for every property and target record. Prove that a sales representative reads targets only through an active assignment to an active campaign and territory, an office-limited member cannot infer sibling-office addresses or counts, and no new table is projected to mobile synchronization. Every write must repeat actor, organization, office, campaign, territory, lifecycle, boundary, duplicate, and authorization checks under the same transaction. A territory boundary edit must reject any polygon that would exclude a saved target.

Run real PostgreSQL races for concurrent map and CRM additions at the same normalized address and within 12 meters. The result must be one reusable property and one target in the campaign. Correct a linked CRM property's address and point, then load it again and verify its stable CRM property ID reuses the original canvassing property and target. Exercise incomplete-address, outside-boundary, ineligible-CRM-source, stale cursor, and boundary-exclusion refusals plus a large sibling-office population without a broad scan. Deployment smoke must probe the protected Canvassing route and a real target-list procedure. No mobile build or visible mobile acceptance change belongs to this release.

Property-loading and manager CSV migration order

Apply the release in this exact order:

  1. 0068_redundant_spencer_smythe establishes the original four-table

campaign, territory, assignment, and audit foundation.

  1. 0074_sturdy_dazzler adds canvassing_properties and

canvassing_targets, their tenant and campaign uniqueness constraints, private read boundary, point-in-polygon and duplicate guards, and the supporting indexes and functions.

  1. 0075_canvassing_scope_retrofit activates the compatible property-loading

scope alongside the web/API release. It installs the saved-target boundary trigger and replaces the legacy campaign, territory, assignment, and audit policies with manage-or-active-assignment reads and manager-only writes.

  1. 0076_amused_smasher adds the private CSV geocode cache, preview batches,

staged rows, strict confirmation guards, and bounded service-only purge. Confirmed evidence is retained and the new relations stay outside PowerSync.

  1. 0078_green_hellfire_club changes the staged-row text constraint so invalid

rows with missing required fields can be retained for a useful preview, and adds the forced-RLS raw-upload ticket ledger plus its single-use preview and bounded cleanup claims. Ready rows still require complete bounded text, coordinates, and no errors.

New environments run the complete journal through 0078 in order; 0077 is the intervening invoice-receipt migration and is not reused by canvassing. An upgraded environment that already received the schema-first 0074 stage must still run 0075 before the property-loading web head is promoted. Do not deploy the web controls against 0074 alone. Apply 0076 and 0078 before deploying the manager CSV web/API head.

After 0075, verify all six canvassing relations have the expected forced-RLS or append-only boundary, the restricted app role can only read the new property and target tables through its authorized scope, manager target writes still pass the trusted-writer guards, active assigned representatives cannot read paused, completed, archived, unassigned, or sibling-office targets, and the two new relations remain absent from PowerSync streams.

After 0076, additionally verify that the three CSV preparation relations use forced RLS, ordinary application calls cannot write staging or geocode records directly, confirmation is executable only by the authenticated application role with the exact organization/member request context, and the bounded purge can remove only expired unconfirmed previews. Confirmed batches and rows must remain immutable and excluded from purge and mobile synchronization.

After 0078, also verify that canvassing_csv_upload_tickets uses forced RLS with no ordinary table grants, issuance and preview claims require the exact signed application organization/member context, owner cleanup uses bounded SKIP LOCKED claims, and only the matching UUID lease can release or finalize a path. The ticket relation must remain absent from PowerSync. Its organization and actor foreign keys deliberately use NO ACTION: Clerk lifecycle handling tombstones or deactivates those records so the actor binding survives until raw cleanup retires the ticket. Any future hard-delete repair must purge the organization's tickets and Storage objects first.

Register the production Inngest function purge-expired-canvassing-csv-imports. It runs hourly through the owner/service database connection, deletes no more than 100 eligible previews and claims no more than 25 expired raw uploads per invocation, and queues another bounded invocation when either batch is full. Raw objects are deleted with bounded concurrency; Storage 404 is terminal success, while transient deletion failures receive a short token-owned retry lease. The job returns counts only; raw paths, staged addresses, and owner fields never enter its step result or continuation payload.

For the manager CSV web release, verify the private Supabase Storage bucket canvassing-csv-imports exists and set SUPABASE_CANVASSING_IMPORT_BUCKET=canvassing-csv-imports in Vercel Production and Preview. Keep its 5 MiB object limit and text/csv MIME restriction, permit signed PUT requests from the production and approved preview web origins in Storage CORS. Supabase Storage does not provide a configurable object-lifecycle backstop for this bucket. Instead, V2 records the actor-bound path before it returns the signed URL. A successful preview marks that ticket consumed but keeps its path for one day—well beyond Supabase's two-hour signed upload capability—so reusing an otherwise-valid PUT URL cannot create an untracked raw object. The registered hourly job deletes any object at that path before it retires the expired ticket. Keep SUPABASE_SERVICE_ROLE_KEY server-only. The server issues an actor-bound signed upload path, downloads at most 5 MB through a bounded stream, deletes the raw object before returning a preview, and stores no raw bytes in PostgreSQL.

Production's signed-upload CORS preflight was verified on 2026-08-23 without uploading an object: the response was 200, allowed PUT, accepted content-type and x-upsert, and returned the configured wildcard origin for the unguessable signed URL. Repeat that non-mutating preflight after any Storage or origin-policy change.

Set GOOGLE_MAPS_SERVER_API_KEY to a server-restricted key with the Geocoding API enabled. GOOGLE_MAPS_SERVER_KEY remains a temporary server-only compatibility alias for the existing production secret; the canonical variable takes precedence when both exist. Rotate production to GOOGLE_MAPS_SERVER_API_KEY, verify geocoding, then retire the legacy alias. Never use the browser maps key for CSV geocoding. Missing-coordinate previews permit no more than 100 lookup candidates, use five concurrent requests, reuse the organization-private cache, and cache not_found for one day. Provider failures fail closed before preview staging.

Do not promote the manager CSV controls until the private canvassing-csv-imports bucket, matching SUPABASE_CANVASSING_IMPORT_BUCKET, and either the canonical GOOGLE_MAPS_SERVER_API_KEY or temporary legacy server alias are present in the production environment and the hourly Inngest cleanup registration is visible.

Verify upload-path cross-member denial, exact signed-session ticket binding, single-use preview claims, stale-claim token denial, 5 MB streaming enforcement, raw-object deletion on both valid and invalid source files, one-day abandoned-object cleanup including 404 and transient retry behavior, one-hour preview expiry, invalid-row confirmation denial, source-byte/parsed-row binding, stale territory rejection, replay-safe confirmation, and service-role entry denial. Rows with only one usable coordinate must remain visible as invalid rows with both staged coordinates cleared. Differently named rows within the 12-meter reuse radius of an earlier new row in the same file must be marked as a nearby duplicate and block confirmation, so the approved preview cannot claim two new properties that confirmation would collapse into one. Likewise, when exact and nearby matches resolve to the same existing property, only the first valid row may remain ready; later rows must be duplicate errors. An address whose NFKC normalization exceeds the 1,000-character staging key limit must likewise remain a correctable invalid row rather than aborting the preview insert. All saved-property matching, point-in-polygon checks, and within-file duplicate decisions must use PostgreSQL's authoritative seven-decimal staged coordinates, including half-away decimal ties and exponent-form provider results; never make those decisions from higher-precision source values that confirmation cannot retain. The web response may return at most 100 row summaries even though the private batch can contain 5,000 rows. Confirm all four private CSV relations—geocode cache, preview batches, staged rows, and upload tickets—remain absent from PowerSync and no mobile source or visible mobile behavior changes.

Campaign lifecycle changes, territory mutations, and assignment replacement first lock the actor and relevant assignee members in stable ID order, then take the organization canvassing gate, and only then lock the campaign or territory before writing the requested change. Database triggers independently reject inactive or ineligible assignees, locationless sales reps, forged assignment actors, and member changes that would invalidate an active assignment. Exercise those races on a real two-connection PostgreSQL test environment; the local PGlite harness is single-connection and can pin query order and invariants but cannot prove lock blocking.

Ordinary office administration remains subject to that invariant: a member cannot be deactivated or made ineligible while an active territory assignment still depends on them. Identity-provider removal is the bounded offboarding exception. A Clerk organization, organization-membership, or user deletion locks organizations with FOR NO KEY UPDATE, takes the existing Calendar member-capacity gates, then locks members, the organization canvassing gate, affected territories, and active assignments in that stable order inside one transaction. The weaker organization lock still excludes competing lifecycle writes while remaining compatible with the foreign-key key-share lock used by the deferred list-revision publisher. It therefore avoids a lock cycle between provider offboarding and list invalidation.

Offboarding retires each active assignment without deleting its history, writes one append-only assignment_provider_deactivated audit event per retired assignment, increments each affected territory revision once, and only then deactivates the member. Each audit's actor_member_id is explicitly the affected provider subject, not a claim that the teammate initiated the Clerk deletion; its details carry actorAttribution: provider_subject. A territory's updated_by uses one deterministic affected provider subject for the same required identity anchor, while the assignment's original assigned_by history remains unchanged. Organization deletion applies to every member, membership deletion to that organization/member pair, and user deletion across all of the user's organizations.

Replaying the provider deletion is idempotent: assignments that are already inactive do not produce another audit event or territory revision. The normal statement-level list-revision queue still coalesces the offboarding transaction to one published list revision per affected organization.

clerk_user_lifecycle is a permanent, service-only identity gate with no tenant-role access. Clerk user IDs are not reused, so user.deleted writes its user tombstone before organization, assignment, or member cleanup and never clears it. Membership creation, organization-creator reconciliation, and authenticated bootstrap all lock that user lifecycle first, then take the organization share lock and recheck the applicable user/organization tombstone before any member activation. A delayed organization.created event may still mirror the organization, but skips its deleted creator; a stale bootstrap fails with user-deleted before database side effects. This preserves the global user → organization → member lock order against concurrent user and organization deletion. The migration backfills a non-deleted lifecycle row for every existing Clerk member. A database activation trigger establishes and locks the user lifecycle and organization share gate for active direct inserts, rejects Clerk user-ID changes, and applies the same deleted-user and deleted-org backstops to inactive-to-active updates. An inactive provider-removal tombstone insert grants no access and returns without taking either gate; this prevents a membership-deletion transaction that already owns the organization gate from inverting user.deleted's user → organization order. Because a reactivation update already owns the member row, it checks both deletion gates with no-wait locks and fails closed with clerk-user-lifecycle-busy or clerk-org-lifecycle-busy when a deletion owns either gate, avoiding a reverse-order deadlock. No later event, stale session, direct write, or administrative update can therefore restore a provider-deleted identity.

The standard GitHub CI job provisions an ephemeral PostgreSQL service named cleanestimate_canvassing_test, applies the current migration chain, and runs the two-connection race suite automatically. No repository secret or external database is used. A passing required CI check is the release evidence for these lock-order races.

To run the same suite outside CI, provision a disposable database with the current migrations whose literal URL pathname is exactly /cleanestimate_canvassing_test, and set CANVASSING_TEST_DATABASE_URL only for the test command:

pnpm --filter @ce/api exec vitest run test/canvassing-postgres-races.test.ts

The harness performs whole-database teardown and accepts it only when the raw database pathname is the exact allowlisted value /cleanestimate_canvassing_test. A generic test or testing marker is not sufficient. Any percent encoding or raw Unicode encoding in the pathname is rejected instead of decoded. The harness compares DATABASE_URL using the same driver-equivalent raw database identity and refuses a test URL that targets the same canonical host, effective port, and database. Protocol aliases, credentials, an omitted default port, or query settings cannot bypass that comparison. After URL query-name decoding, the exact postgres.js startup identity keys database, user, and client_encoding are rejected in both URLs, including empty, repeated, or percent-encoded spellings. Those keys can override the database, user, or encoding sent in the PostgreSQL startup packet even though the authority still controls the socket. Every decoded query key and value, including every duplicate, is also rejected if it contains a C0 or DEL control character. This prevents an encoded NUL from terminating one startup field and synthesizing a later database or user field. Other query values remain available.

URL authority validation is deterministic and matches the connection identity used by postgres.js 3.4.9. Each raw authority may contain at most one literal @, because the URL parser and driver otherwise disagree about which one ends the credentials. The username and password are decoded once to match the driver; malformed percent encoding or a decoded C0/DEL control character fails closed. Safe encoded credential delimiters such as %40, %2c, and %3a remain supported before that sole @, and percent encoding remains available in safe query values. Any percent sign in the raw host/port portion is rejected. A raw Unicode hostname is converted to percent encoding by the URL parser and therefore also fails closed. Any raw comma in the host/port portion is rejected as a multi-host connection. The remote opt-in does not relax the single-host requirement.

Accepted host identities are limited to raw ASCII LDH DNS names, ASCII punycode names, and unambiguous dotted IPv4 addresses. DNS names are lowercased and stripped of every terminal DNS root dot; labels must be nonempty, no more than 63 characters, contain only letters, digits, or hyphens, and cannot begin or end with a hyphen, while the full name is limited to 253 characters. All bracketed IPv6 connection URLs, including loopback, unspecified, and IPv4-mapped IPv6 forms, are rejected because postgres.js 3.4.9 does not parse their host identity consistently with the URL parser.

localhost, every *.localhost name, and all dotted IPv4 addresses in 127.0.0.0/8 collapse to one loopback identity. A destructive target at 0.0.0.0 is rejected even when the remote override is present. If DATABASE_URL uses 0.0.0.0 as its local-listener host, the canonical comparison treats it as local-equivalent so a loopback test URL cannot evade the same-database check. Ambiguous legacy numeric IPv4 spellings, including short, single-integer, hexadecimal, and octal forms, are rejected in either URL instead of relying on parser-dependent interpretation. The harness intentionally does not resolve arbitrary DNS or CNAME records; any remaining valid ASCII hostname is remote and requires the explicit destructive opt-in below.

The standard CI URL uses the exact database name on loopback and needs no override. A deliberate remote disposable database with that same exact name additionally requires CANVASSING_ALLOW_DESTRUCTIVE_REMOTE_TEST_DATABASE=I_UNDERSTAND_THIS_IS_DESTRUCTIVE on the same test command. Do not set that override globally or use it to point at a shared, staging, or production database. Local runs without a dedicated URL clearly report the real-PostgreSQL suite as skipped while retaining the URL-safety tests; the required GitHub CI run must execute, not skip, all four race cases. A passing run must observe actual lock waits for campaign completion versus territory editing and for both member-change/assignment race orders, including assignment replacement against a team-member display change; the latter must commit without deadlock and invalidate the pre-race list cursor. Do not replace this evidence with a sleep-based timing assertion.

The bootstrap contract defaults campaign, selected-campaign territory, and eligible-member pages to 50 rows, caps each requested page at 100 rows, and returns opaque continuation cursors. Eligible-member paging uses four materialized, index-backed substreams: selected-office named members, selected-office unnamed members, named locationless owners/admins, and unnamed locationless owners/admins. The two selected-office streams allow active owners, admins, and sales representatives; the global streams allow only active owners and admins. Each substream applies its continuation seek and a literal hard LIMIT 101 before PostgreSQL combines the candidates. A fresh page or named-member continuation therefore considers at most 404 rows, and a null-display-name continuation considers at most 202 rows, regardless of the requested page size. The final PostgreSQL merge alone applies the requested limit plus one, preserving the 50-row default and 100-row API maximum. Keeping the substream ceiling literal prevents generic prepared plans from replacing the ordered index paths with unstable bitmap scans and pre-limit sorts.

Four matching covering partial indexes back those paths: organization, office, display name, and ID for named local members; organization, office, and ID for unnamed local members; organization, display name, and ID for named global managers; and organization plus ID for unnamed global managers. The ordered indexes carry the remaining email and role projection, while null-name and null-location branches project typed null values. Named continuations use the (display_name, id) index seek, while unnamed continuations seek directly on id. The final PostgreSQL merge preserves display_name ASC NULLS LAST, id, including duplicate names, without a JavaScript collation merge, deep offset filtering, or an organization-member sequential scan under row-level security. The PostgreSQL plan regression uses large named and unnamed local/global populations and requires four bounded index-only scans for fresh/named paging, or the two null indexes after a null cursor; bitmap and sequential scans fail the gate.

Campaign paging keeps two explicit index paths. A member pinned to one office must have exactly one app_location_ids() entry; any other non-* location scope fails closed with canvassing-location-scope-invalid. Every default, selected, fresh, same-timestamp continuation, older continuation, and selected territory-campaign read then includes that office equality and uses (org_id, location_id, status, updated_at DESC NULLS LAST, id). An unrestricted * administrator retains the organization-wide path on (org_id, status, updated_at DESC NULLS LAST, id). Selected-first behavior, lifecycle/status priority, and cursor ordering do not change.

The PostgreSQL campaign-plan regression runs as app_user with row-level security and a forced generic plan. It seeds a bounded local population plus a much larger sibling-office population with adversarial timestamp collisions. Fresh, same-timestamp, and older office continuations must each place location_id in the location index condition, avoid sort nodes, use one scan, and return at most the requested limit plus one; a newer sibling-office campaign must never displace the selected office's default. The unrestricted administrator path must continue using the organization-wide campaign index.

The territory query must apply its selected-campaign cursor and page limit before joining and aggregating assignment summaries; a bounded response must not aggregate every assignment in a large campaign and discard the extra territories afterward. Every returned territory carries the identity summary for its active assignees even when those members fall outside the current eligible-member page. The web editor pins those assigned members into its choices so they remain named, selected, and removable while member pagination continues. Campaign cursors carry selected-first rank, V1 lifecycle rank, update time, and ID; member and territory cursors carry their V1 display-name/name sort key and ID. Every cursor is versioned, bound to the selected campaign, and carries the latest committed per-organization canvassing change stamp. Relevant writes use statement-level transition tables to enqueue distinct affected organizations in a protected pending table, coalescing repeated statements to one pending row per (org_id, transaction_id). A deferred constraint trigger publishes and consumes that row through the organization counter at commit. Revision publication is therefore O(distinct organizations in the transaction), not O(affected rows), while the commit gate still serializes concurrent mutations for one organization. A transaction that started earlier but commits later still publishes after the transaction ahead of it and invalidates a cursor issued before that late commit.

Under the normal READ COMMITTED service transaction, bootstrap reads the current organization revision before the location, campaign, member, and territory page queries, then reads it again as its final SQL query. If the ending revision differs, bootstrap throws canvassing-pagination-cursor-stale and returns none of the mixed page data. This applies to initial/no-cursor loads as well as continuations. A continuation with a different campaign scope is rejected separately. This preserves V1 scanning order without claiming mutable fields are immutable. Clients must return cursors unchanged and must not inspect or synthesize them.

For any stale bootstrap response, the web client closes and resets both campaign and territory editors, then makes exactly one no-cursor bootstrap request for the selected campaign. A successful retry shows the latest-list notice without implying that the stale draft was preserved. If the retry is also stale, the client must not loop: it shows Canvassing changed again while refreshing. Select Retry to load the latest list. and renders Retry in both the empty and populated workspace states.

Archived campaigns must not render Edit campaign or territory mutation controls. The manager guidance must require Restore to draft before either campaign details or territories can change. Completed campaigns may retain the campaign-detail editor, but territory controls remain unavailable until Reopen succeeds. Test these guards from the rendered lifecycle state rather than relying only on service rejection.

Polygon validation is bounded at 100 points and rejects repeated vertices and self-intersection. Cross-territory overlap detection is not part of this foundation and must not be represented as an automatic safeguard.

This is a release checklist. Source code, automated tests, or a successful local export do not prove that database migrations, server functions, native binaries, store credentials, or background location behavior are live in production.

Do not enable canvassing for a live organization until every applicable item below is confirmed in the target environment.

1. Database and Server

  • Apply the complete canvassing migration chain in timestamp order. It includes the foundation, proposal-delivery idempotency and privacy, estimate scheduling and customer notes, geocode cache, manager workflows and settings, server authority, missing-property intake, authenticated weighted rate limits, atomic AI quota/replay, scalable retention, 12-meter import deduplication, incremental rollups, durable visit claims, field-integrity evidence, verified goal/leaderboard progress, immutable GPS replay fingerprints, visit-request ledger retention, and the ordered release tail: 20260723254000_organization_reporting_timezone.sql, 20260723255000_canvassing_filtered_leaderboard.sql, 20260723256000_canvassing_representative_drill.sql, 20260723257000_canvassing_leaderboard_emphasis.sql, 20260723258000_pricing_catalog_rls_hardening.sql, 20260723259000_canonical_estimate_acceptance.sql, 20260723260000_estimate_acceptance_revision.sql, 20260723261000_mobile_residential_proposal_atomic_sync.sql, 20260723262000_proposal_delivery_revision_guard.sql, 20260723262500_mobile_estimate_photo_idempotency.sql, 20260723263000_public_web_canonical_acceptance.sql, 20260723263500_job_photo_storage_hardening.sql, 20260723264000_signed_estimate_contract_immutability.sql, 20260723265000_estimate_acceptance_reconciliation_lease.sql, 20260723266000_estimate_acceptance_outbox.sql, 20260723267000_database_runtime_lint_hardening.sql, 20260723268000_canvassing_bulk_target_rollup_scale.sql, and 20260723269000_service_role_runtime_grants.sql.
  • Confirm PostGIS is available and the spatial columns and indexes were created.
  • Confirm RLS is enabled on every canvassing table.
  • Verify the target environment uses the intended live Supabase project for the web app, mobile app, Edge Functions, Auth, and Storage.
  • Apply the chain to a production-like staging database running real PostgreSQL with PostGIS. Run the SQL/RLS test suite against that database and verify functions, triggers, transition-table rollups, constraints, indexes, grants, service-only routines, tenant isolation, and cron jobs. Source-text migration tests are not a substitute.
  • The clean local Supabase PostgreSQL 17/PostGIS chain through seed now passes with zero database-lint errors and a 24-test pgTAP authorization and scale matrix. CI repeats the clean apply, lint, and pgTAP checks on every change. The matrix includes a 10,000-target single-statement insert, exact target/counter/disposition checks, and trusted service-role table/sequence privileges. This proves clean-schema compatibility but does not replace applying the chain to an upgraded production snapshot in the target staging environment.
  • Confirm the raw-location retention job exists and makes progress in bounded batches while preserving shift summaries. When pg_cron is unavailable, invoke canvassing_run_location_retention hourly from an authorized service-role scheduler.
  • Confirm the visit-request retention job removes only completed claims older than 45 days, records bounded backlog health, and preserves reserved/recoverable claims. When pg_cron is unavailable, invoke canvassing_run_visit_request_retention(10,5000) hourly from an authorized service-role scheduler.
  • Confirm the proposal-delivery ledger retention job removes requests and recipient PII older than 90 days. When pg_cron is unavailable, invoke proposal_delivery_run_retention hourly from an authorized service-role scheduler.
  • Confirm the AI result reconciler and 24-hour replay cleanup jobs are scheduled. When pg_cron is unavailable, schedule canvassing_reconcile_ai_requests and canvassing_run_ai_request_cleanup through an authorized service-role runner.
  • Confirm every organization has a valid IANA reporting timezone or the documented UTC fallback. Verify canvassing_get_user_goal_progress and canvassing_get_reporting_period_bounds remain service-role-only, require the requested actor to be an active organization member, and return PostgreSQL-derived Today, Week, and Month boundaries for operational, XP, and balanced leaderboards. Exercise repeated and skipped local midnights, not only ordinary daylight-saving transitions.
  • Verify the filtered canvassing_get_verified_leaderboard overload remains service-role-only, checks the requested actor's active membership, rejects cross-organization or non-live campaign/territory scopes, and validates known XP event/source pairs against their live source records. Confirm the period-bounded attribution and sent-estimate plans use the migration's supporting indexes without reintroducing the redundant attribution entity index. Keep the previous four-argument overload available during the rolling application deployment.
  • Confirm authenticated roles cannot insert, update, or delete user_xp_events or user_gamification_profile, while service_role retains explicit write grants. Run the authenticated-denial behavior regression as well as the grant inspection. Exercise signed negative reversal events and confirm period XP preserves the correction while balanced scoring gives it no positive contribution.
  • Confirm missing or disabled gamification settings return disabled, rank-only standings; a representative with rank-only privacy receives only their own row, while an authorized owner or manager retains administrative oversight.
  • Confirm the manager-settings table constraint, service-only update function, shared TypeScript allowlist, mobile response validator, and native formatter all accept the same featured-metric set: balanced, XP, doors, contacts, qualified leads, appointments, estimates sent, sales, revenue, contact rate, appointment rate, and close rate. Confirm rate features render as percentages and only XP changes the main ranking away from balanced score.
  • Verify canvassing_get_representative_drill_targets remains service-role-only and requires an active owner or manager actor plus an active organization representative. Confirm it rejects invalid or non-live campaign/territory scope, bounds date ranges and pagination, and applies representative, period, campaign, territory, disposition, and normalized-address filters before returning only target IDs, last activity, match counts, and a total.
  • Deploy the current proposal-delivery function after its database migration and secrets are ready.
  • Run tenant-isolation checks with at least two organizations before enabling real field data.
  • Verify authenticated limits are keyed to the authenticated organization and principal, not a caller-controlled IP, and that bulk imports consume weighted capacity.
  • Verify manager team-location reads follow bounded cursor pagination and cannot expose another organization or an unauthorized representative.
  • Verify active-member pricing, service, add-on, price-book, custom-template,

quote-core, estimate, proposal, job, lead-source, and organization reads do not cross tenants. Confirm authenticated clients cannot mutate residential proposals, organization rows, or quote-core records directly. Separately verify that the compatibility policies allow only commercial, fleet, and holiday-lights proposal inserts/updates and limit a sales representative to their own created_by rows.

  • Exercise sign_estimate_with_canonical_acceptance with the service role and

confirm it atomically rejects a changed contract-only acceptance revision, total, pricing version, terms text, terms timestamp, organization, or existing signature. Confirm a viewed_at/view_count telemetry update does not advance the acceptance revision, and confirm authenticated clients cannot call the function directly.

  • Verify mobile proposal sync recalculates canonical price and scope in one

transaction, requires the reviewed bundle revision for a sent proposal, and cannot let delivery claim a superseded proposal revision. Replay the same proposal, delivery, and client photo identifiers with both matching and conflicting payloads.

  • Confirm new estimate-photo objects use the private

organization/estimate/client path policy, malformed legacy paths fail closed, authorized reads return only short-lived signed URLs, and the storage hardening migration's deferred legacy-row validation is completed only after the production-like backfill audit passes.

  • Exercise the public web package, direct estimate, and maintenance canonical

acceptance flows against the current server-owned review. Verify staged package selection is not acceptance, exact ambiguous signature retries are idempotent, and a signed maintenance child can safely resume interrupted finalization without a second signature.

  • Race an original post-sign request against an exact retry and confirm the

service-role-only reconciliation lease permits only one job/automation pass, an active claim reports that finalization is still in progress, and a stale claim can recover after the default 120-second lease window without creating a second job or rehash message. The database bounds an explicitly requested lease to 15–600 seconds; production currently uses the 120-second default.

  • Accept an acceptance-owning estimate while forcing each secondary write to

fail in turn. Confirm the terminal estimate and its estimate-acceptance-effects:{estimateId} background job commit together, the minute worker authenticates internally, and retries eventually create exactly one accepted quote event, one webhook delivery per matching config, one Won transition, and one copy of every earned XP event. Confirm maintenance offers and residential package mirrors do not enqueue a second acceptance job, while linked fleet, commercial-building, and holiday-lights estimates do retain replay. Exercise retry exhaustion and verify queue metrics and dead-letter alerting are operational before enablement.

  • Confirm accepted, signed, fingerprinted, snapshotted, and

maintenance-accepted estimates reject contract mutation, downgrade, and ordinary hard deletion at both the API and database layers. Verify the service-role-only legal-erasure RPC is unavailable to public, authenticated, and ordinary service-role DELETE writers, and that the checked sole-member organization-erasure path fails before deleting the Auth user if database cleanup does not complete.

Migrations are additive and should be reviewed before rollout. Do not reset the production database or the mobile WatermelonDB store to introduce canvassing.

2. Web Environment

Required application configuration includes:

VariablePurpose
NEXT_PUBLIC_SUPABASE_URLAuthenticated web and API Supabase project
NEXT_PUBLIC_SUPABASE_ANON_KEYBrowser Supabase client
SUPABASE_SERVICE_ROLE_KEYServer-only scoped data operations
SUPABASE_CANVASSING_IMPORT_BUCKETPrivate actor-bound canvassing CSV uploads
NEXT_PUBLIC_GOOGLE_MAPS_API_KEYAuthenticated web command-center map
GOOGLE_MAPS_SERVER_API_KEYCanonical private server geocoding and cache population key; the temporary server-only GOOGLE_MAPS_SERVER_KEY alias is accepted during rotation
OPENAI_API_KEYCanvassing AI provider access when AI is enabled
NEXT_PUBLIC_APP_URLCanonical customer and application link origin
CUSTOMER_TOKEN_SECRETStable signing secret for customer estimate and proposal links

The canvassing import path prefers GOOGLE_MAPS_SERVER_API_KEY when an address needs geocoding and temporarily accepts the server-only GOOGLE_MAPS_SERVER_KEY alias while production rotates to the canonical name. It does not fall back to a browser key. Keep both server variables out of browser bundles and EAS environments.

Use an authorized GET /api/health/canvassing or HEAD /api/health/canvassing request as the feature-specific release gate:

curl --fail \
  -H "Authorization: Bearer $CRON_SECRET" \
  https://app.cleanestimate.pro/api/health/canvassing

A 200 response proves that the private server geocoding key is present and that Supabase exposes every canvassing relation and RPC referenced by the web/mobile runtime plus the atomic mobile proposal handoff. A 503 response means canvassing must remain disabled even when the general /api/health endpoint is green. The route rejects unauthenticated requests before contacting Supabase, returns missing schema path names only to the authorized release monitor, and never returns credential values.

Proposal email/SMS also requires the existing delivery-provider secrets in the Supabase function environment, including the configured Resend and Twilio credentials for the channels your organization uses.

The Vercel ignored-build command must always return a build-required result for the production environment. Preview deployments may continue to skip mobile-only, documentation-only, migration-only, script-only, and test-only changes. This prevents a release-evidence commit at the tip of a multi-commit main push from masking earlier web-runtime changes in the same release.

Use real target-environment secrets for release verification. Placeholder Firebase files, example Supabase keys, unrestricted Maps keys, missing AI credentials, or test delivery credentials do not satisfy this gate. Confirm every secret by exercising its actual server, native, AI, geocoding, or delivery path without printing the value to logs.

3. Native Build Environment

All EAS profiles must point to the intended API origin and Supabase environment. Confirm:

  • EXPO_PUBLIC_APP_URL
  • EXPO_PUBLIC_SUPABASE_URL
  • EXPO_PUBLIC_SUPABASE_ANON_KEY
  • EXPO_PUBLIC_EAS_PROJECT_ID
  • EXPO_PUBLIC_GOOGLE_MAPS_API_KEY or the supported legacy EXPO_PUBLIC_GOOGLE_PLACES_API_KEY for mobile address search
  • EXPO_PUBLIC_GOOGLE_MAPS_ANDROID_API_KEY
  • GOOGLE_SERVICES_JSON or the equivalent EAS file secret
  • APP_ENV

Each development, preview, and production build profile selects its matching named EAS environment. The Supabase URL and anonymous key are intentionally absent from committed profile env blocks so a stale repository value cannot override the remote EAS environment. Before every build, confirm the selected remote Supabase URL matches the web application's target project.

Inspect the remote EAS environment for every release profile; a local .env or successful static export does not prove those remote values or file secrets exist. Verify the mobile Maps/Places key, Android Maps SDK key, and GOOGLE_SERVICES_JSON file secret resolve in the intended EAS project without printing their values.

The public mobile Maps/Places keys are client identifiers, not private server keys. Restrict them to only the APIs and application identities they require. Restrict EXPO_PUBLIC_GOOGLE_MAPS_ANDROID_API_KEY in Google Cloud to:

  • Maps SDK for Android
  • Android package com.cleanestimate.pro
  • Every certificate fingerprint used by development, preview, and production signing as appropriate

The iOS app uses the supported native map provider and does not need the private server geocoding key.

The native config plugin must generate Android allowBackup="false" plus file, database, shared-preference, external-storage, cloud-backup, and device-transfer exclusions. The iOS application must mark its Documents directory as excluded from backup. Inspect the generated signed projects; source plugin tests alone do not prove the release binaries contain these controls.

4. Location and Notification Configuration

The native configuration must retain:

  • iOS UIBackgroundModes with location
  • iOS When In Use and Always-and-When-In-Use location explanations
  • Android fine, coarse, background-location, foreground-service, and foreground-service-location permissions
  • Android foreground tracking notification support
  • Android camera access for estimate photos, with image-picker microphone

access explicitly disabled because the field workflow does not record audio

  • iOS supported background-location indicator behavior

Background location must be explained to the representative before the permission prompts. Tracking must begin only after the server starts an active shift and must stop when the shift ends, the user signs out, or the organization changes. If an attempted upload of a just-collected background batch receives an authentication or permanent failure, that batch must enter the scoped durable queue before tracking suspension and the interruption event. Context-change teardown must also preserve the departed organization/user's queue: if GPS or other canvassing work remains pending, or authentication blocks a flush, the shift-end write is queued behind that work while downloaded canvassing views are cleared. A different account or organization must not be able to read or process the quarantined queue.

Expo Go is not a valid background-location acceptance environment. Use a development, preview, or production native binary.

5. EAS and Store Credentials

Before creating or submitting production binaries:

  • Confirm EAS remote iOS distribution certificates and provisioning profiles.
  • Confirm the App Store Connect account, app record, bundle ID, agreements, and privacy answers.
  • Confirm Google Play app signing, package record, background-location declaration, data-safety answers, and production track access.
  • Supply the Google Play service-account file securely when using EAS submit. mobile/google-service-account.json is intentionally ignored and must not be committed.
  • Confirm the Android app bundle and iOS archive use the intended production channel and runtime version.
  • Confirm production map-key restrictions include the actual store signing certificate.

Store submission credentials are external prerequisites. A successful TypeScript check or Expo export does not validate them.

Privacy counsel or another authorized reviewer must approve the employee location notice, raw GPS retention choice, AI/coaching disclosures, proposal-delivery retention, and applicable labor/privacy terms. App Store and Google Play privacy, background-location, data-safety, and review approvals are separate release gates.

6. Automated Verification

Run the current repository checks from the web and mobile workspaces:

Web: lint, TypeScript, full tests, production build
Mobile: TypeScript, full Jest suite, Expo Doctor
Native bundle: Android export and iOS export
Docs: TypeScript and production build

Also verify:

  • Migration security and manager-workflow tests
  • Campaign, territory, imports, reporting, exports, settings, AI, GPS, estimate acceptance, scheduling, and proposal-delivery tests
  • Durable visit-claim tests for exact replay, conflicting fingerprints, active leases, stale recovery, CRM reuse, communication suppression, and manager-review evidence
  • GPS batch tests for exact replay, immutable payload conflicts, wrong actors, malformed RPC summaries, and replay attempts after raw-location retention
  • Visit-request retention tests for the 45-day cutoff, bounded cleanup, reserved-claim preservation, scheduler fallback, and health-state access
  • RFC-style CSV tests for quoted multiline fields, escaped quotes, row/cell limits, duplicate headers, malformed quoting, and exact row widths
  • Concurrent 12-meter deduplication tests across CSV, CRM, map entry, and field-created properties
  • Incremental-rollup reconciliation tests against full refresh results
  • Atomic AI quota tests for reservation, concurrency, exact replay, staged-result reconciliation, indeterminate provider outcomes, and replay cleanup
  • Local proposal/canvassing scope isolation; sign-out, user-change, and organization-switch cache cleanup; scoped GPS and shift-end queue preservation and ordering; and native backup-exclusion tests
  • Canonical residential bundle scope, digest, expiry, cache-fallback,

permission, rate-limit, catalog, add-on option, custom-template, and published-Suds tests

  • Shared service-measurement mapping and Step 3 validation for every supported

residential service, including configured types and whole-window counts

  • Residential sync tests for server recalculation, exact sent revision/total,

stale catalog and permission rejection, nonbillable incomplete drafts, and delivery dependency blocking

  • Builder autosave tests for first-edit draft creation, debounce/background

flush, in-flight ordering, proposal-generation races, explicit-save reuse, and account isolation

  • Organization API allowlist/logo-scope/rate-limit tests and pipeline

ownership, transition, acceptance-authority, and optimistic-concurrency tests

  • Canonical acceptance fingerprint, signature service, public estimate/shared

quote/package/maintenance routes, mobile detail/accept route, exact retry, stale estimate/terms, package-stage ordering, unsigned-maintenance rejection, and PostgreSQL compare-and-set tests

  • Transactional accepted-estimate outbox tests for first-transition enqueue,

tenant-bound internal authorization, maintenance/package-child exclusion, deterministic quote/webhook/XP replay, strict failure retry, and webhook conflict inference

  • Private photo-path integrity, estimate/client binding, short-lived signed

reads, private stored-object deletion, in-flight upload cancellation, and fail-closed unknown-bucket tests

  • Authenticated-principal rate-limit and team-location cursor-pagination tests
  • Assigned-target property-context tests for organization/live scope, territory access, the independent Clients + Estimates and Clients + Jobs permission gates, and explicit unavailable states
  • Fresh target reconciliation tests that project a server do-not-knock restriction across every downloaded target for the property without rolling back a newer unsynced visit
  • Organization-timezone and PGlite regression tests for UTC fallback, spring and fall daylight-saving transitions, repeated and skipped local midnight, exact-boundary requests, identical period bounds across operational, XP, and balanced leaderboards, and matching organization-local calendar edges across reports, property lists/details, representative drills, routes, exports, and Manager AI. Verify the web filter labels expose the IANA zone, reset uses the organization-local current date plus the prior 29 dates, and report headings and daily activity buckets use and label that same zone. Reject impossible Gregorian dates and explicit timestamps that omit Z or a numeric UTC offset.
  • Filtered-leaderboard tests for authorized campaign and territory scope, Sales reps only, XP-or-balanced main ranking, independent emphasis of every supported count, currency, and rate metric, qualified-lead detail projection, top-25-plus-own bounding, a maximum 26-profile identity lookup, empty-result short-circuiting, signed XP reversals, source actor and event/source-pair validation, live-environment isolation, fail-closed privacy/settings behavior, and response PII minimization
  • Representative-drill migration and route tests for service-only execution, owner/manager authorization, cross-organization and non-live scope rejection, representative/date/campaign/territory/disposition/address filtering, distinct door/visit/estimate/sale source semantics, exact target_id detail filtering, campaign/territory disambiguation for reusable properties, visit-cohort conversions, deterministic ordering, page bounds, malformed RPC rejection, organization-scoped target hydration, and focus-specific property-detail filtering
  • Mobile leaderboard tests for every required activity, conversion, revenue, and XP card field plus qualified leads; visit-cohort contact/appointment/close rates versus activity-period outcome counts and revenue; compact-to-full-screen scroll behavior; per-card featured-metric promotion and highlighting; open-only 120–150-second staggered polling; stable-principal rate limiting; a 30-second organization/rank/filter/reporting-bound score cache that reapplies authorization, privacy, and profile selection per request; private non-cacheable HTTP responses; exact version 4 device-cache provenance; self-only Current user persistence with no teammate identifiers or names; five-minute expiry and 30-second future-skew rejection; exact echoed filter and ordered-bound validation; rejection and writable-maintenance purge of legacy version 3 snapshots; authorization purges; disabled/rank-only purges; a failed-purge privacy barrier that blocks subsequent offline identity fallback; and malformed-response rejection
  • No hard-coded service-role or AI provider keys are present in the mobile bundle
  • The web and mobile API origin returns the expected authenticated response
  • Live and test canvassing records remain separated

The final merged local release-candidate record on July 24 is:

  • Web Vitest: 441 files / 2,052 tests
  • Web TypeScript and production build: passed; 434 routes/pages
  • Strict lint over every changed TypeScript/TSX file: 0 warnings / 0 errors
  • Web production-only and full dependency audits: 0 vulnerabilities
  • Mobile Jest: 60 suites / 402 tests
  • Mobile TypeScript and Expo Doctor: passed; 19/19 Doctor checks
  • Android/iOS static exports: passed; 4,403 / 4,377 modules and

9.3 / 9.2 MB Hermes bundles

  • Clean Supabase PostgreSQL 17/PostGIS migration and seed: passed; database

lint reported 0 errors and the pgTAP authorization matrix passed 24/24 tests, including a 10,000-target set-based rollup fixture

  • Docs TypeScript and production build: passed; 152 static pages

The latest completed mobile dependency audit in this release task reported no vulnerabilities. A final registry refresh could not complete because npm returned HTTP 503 on three attempts. These local results remain repository evidence only; static exports are not signed native builds, and the migration, load, authenticated-deployment, physical-device, store, legal, and privacy gates below remain open.

Accepted-estimate secondary effects now have a durable replay boundary. The first acceptance-owning estimate transition to accepted or signed queues a tenant-scoped internal job in the same transaction. Quote events, each configured webhook delivery, the Won transition, and XP ledger entries use deterministic keys, so the original request and a retry can overlap without duplicating them. Maintenance offers and residential package mirrors are excluded; linked fleet, commercial-building, and holiday-lights estimates retain replay. The release gate is now operational evidence that the scheduled background worker, internal secret, queue monitoring, retry progress, and dead-letter alerting work in the production-like target environment.

Run the dedicated read-only staging profile with independent temporary admin cookies and native bearer sessions:

npm run test:stress:canvassing -- \
  --base-url=https://staging.cleanestimate.pro \
  --cookie-file=.secrets/canvassing-admin-cookies.txt \
  --mobile-session-file=.secrets/canvassing-mobile-sessions.json \
  --campaign-ids=campaign-uuid \
  --territory-ids=territory-uuid \
  --representative-ids=representative-uuid

Never commit the credential files. Give a native session "managerAccess": true only when the account really is an owner or manager; the manager-only team-location scenario is omitted for ordinary representatives. The default profile ramps through 10, 50, 150, and 250 paced sessions and requires at most 1% errors, at most 2% throttling, 1200ms overall p95, 1500ms authenticated-route p95, and at least 20 samples per included authenticated scenario. Supply at least half as many independent principals as peak sessions so account-level protection is not mistaken for system capacity.

A bounded local proof against 10,000 assigned targets completed 65/65 health and representative-native requests with 0% errors, 0% throttling, and 201ms p95. It validates the harness and local read path, not production capacity. After the full staging migration, also exercise simultaneous imports/manual field additions, visit and CRM retries, GPS batches and retention, command-center reporting/rollups, paginated representative source drills, paged team locations, proposal delivery, and AI reservation/reconciliation. Record latency, errors, duplicates, lock waits, backlog, database connections, CPU/IO, index growth, and dead tuples. The deterministic 1,000-participant leaderboard regression and the local proof are not substitutes for this target-environment run.

An export verifies JavaScript bundle creation. It does not compile store-native projects, validate signing, prove remote EAS Maps/Places or GOOGLE_SERVICES_JSON configuration, or prove locked-screen GPS behavior.

The July 24 release audit found and removed stale committed Supabase overrides from every EAS build profile. The named remote development, preview, and production environments point at the same Supabase project used by the web deployment. The mobile Maps/Places and Android Maps variables are now present in all three remote EAS environments and match the live estimator Maps credential. Android internal build 45 completed successfully from merged main commit 18a14345 with those variables and the live Supabase configuration. Native emulator acceptance also verified the map, disposition sheet, address list, status chips, territory selector, and active-shift controls. The territory selector is now height-bounded so it cannot squeeze the field map, and the active-shift controls remain elevated above the native map surface on Android.

The final Google Cloud application restriction still needs a signed-device check against package com.cleanestimate.pro and the active EAS signing certificate. GOOGLE_SERVICES_JSON also remains absent: every local file found during the audit identifies itself as a placeholder and must not be promoted as production Firebase configuration. The first iOS internal build remains blocked until an interactive Apple signing session creates credentials suitable for internal distribution. A current signed Android physical-device pass, real Firebase configuration, and the first signed iOS/TestFlight build therefore remain launch gates.

7. Physical-Device Acceptance

Complete and sign this matrix on at least one current physical Android phone and one current physical iPhone using a release-signed preview or production-candidate build:

  1. Sign in to the intended production-like organization.
  2. Download an assigned territory and verify map and list paging.
  3. Start a shift and grant precise foreground and background access.
  4. Confirm Android shows the persistent tracking notification and iOS shows supported location activity.
  5. Lock the phone and leave the app backgrounded long enough to collect route points.
  6. Turn off cellular and Wi-Fi.
  7. Long-press a missing door, confirm its pending offline marker cannot record an outcome, and later verify private address resolution.
  8. Open a known property and verify distance, current disposition, attempts, do-not-knock state, CRM warning, recent estimates, and prior services. Repeat with a role that has canvassing access but lacks the underlying estimate/job permissions; confirm those sections say they are unavailable rather than showing data or a false empty history. Reopen the allowed property offline and confirm the history is visibly cached.
  9. Record multiple door outcomes, including a required note and follow-up.
  10. Confirm Interested or Appointment Set, then immediately start an estimate. Verify the newly returned client/lead identifiers and entered name, phone, and email are prefilled and remain linked.
  11. Mark a reusable property Do Not Knock while offline and confirm every downloaded target for that property is blocked immediately, including a target in another campaign. Restore service and verify the organization-wide server state. From a second signed-in device with a stale target page, open that property and confirm the fresh context blocks every downloaded sibling target before another normal outcome can be saved.
  12. Create a local residential estimate while offline. Verify the first

meaningful edit autosaves, every selected service requires its real measurement/type, the cached bundle is visibly scoped and current, and no customer signature can be captured from the local-only proposal.

  1. Restore connectivity and verify property, visit, CRM result, and proposal

sync once without duplicates. Reopen the canonical server estimate and confirm the customer sees the exact scope, total, pricing version, and current organization terms before signing. Change the estimate or terms after opening the sheet and confirm the stale signature is rejected and a fresh sheet clears the old signature/consent. Retry an intentionally ambiguous save with the identical PNG, signer, and timestamp and confirm idempotent success. Select each package and confirm selection only stages a fresh canonical review; acceptance and automation must wait for the matching signature. Turn off a service or add a note and confirm the page requests a revised estimate rather than accepting a browser-calculated total. Confirm maintenance acceptance cannot complete without its matching signature. Also confirm exact GPS replay succeeds once and a deliberately changed replay of the same GPS identifier is rejected.

  1. Simulate a lost visit response, a concurrent retry, and a stale recovery. Verify one visit/CRM result, retry-window behavior, suppressed recovery communications, and manager-review evidence.
  2. Use a quick-estimate template and verify it prefills approved values without bypassing pricing or Red Line controls.
  3. Open Goals & team standings and verify the compact field-screen control expands to a full-screen scrollable panel. Exercise Today, Week, Month, authorized campaign, authorized territory, and Sales reps only filters. Confirm each card shows rank, representative, verified doors, contacts, qualified leads, appointments, estimates sent, sales, revenue, contact rate, appointment rate, close rate, and XP. Exercise every featured-metric choice, confirm counts, currency, and rates are formatted correctly, and confirm the selected value is promoted and highlighted on every card. Confirm the top 25 plus the current user's own row; confirm XP ranks by signed XP while every non-XP featured metric retains balanced main ranking; and confirm polling occurs only while the panel is open at the staggered 120–150-second interval.
  4. Verify server-derived goal progress, operational standings, signed XP standings, balanced standings, disabled settings, rank-only privacy, and manager oversight on both sides of an organization-local midnight and a daylight-saving boundary. While online, confirm full standings can show the top 25 plus own row. Then go offline and confirm version 4 displays only the signed-in representative under Current user, includes no teammate identifiers or names, shows its download time, and expires within five minutes. Reject a snapshot with mismatched echoed filters or bounds, a download time more than 30 seconds in the future, and a legacy version 3 identity-bearing row; confirm writable cache maintenance removes the legacy row. Force the local purge to fail after a rank-only or disabled response, then go offline and confirm the in-memory privacy barrier does not reopen stale identities.
  5. In the web Reporting leaderboard, set campaign, territory, disposition, address, and organization-local date filters, then select a representative name and the linked Verified doors, Estimates, and Sales values. Confirm each focus returns only the exact verified source targets for that representative and preserved filter scope, paginates without duplicates or omissions, and shows the correct Source representative, per-target match count, Last matching activity, campaign, and territory. Use a reusable property represented by more than one target and verify the labels disambiguate it. Open Details and confirm the request carries the exact target_id plus representative, period, campaign, territory, and disposition scope and limits estimate/sale records to the selected focus. Record a later outcome and verify it updates the original verified-visit cohort conversion without being reassigned to another target. Repeat with a non-manager and a cross-organization scope and confirm access fails closed.
  6. Send and resend to controlled email/SMS recipients and verify the delivery log.
  7. Schedule a follow-up, appointment, and accepted-estimate work action.
  8. End the shift and verify native tracking stops immediately.
  9. Keep the app locked/backgrounded after ending and confirm no new location points arrive.
  10. Revoke location permission mid-shift and confirm the interruption warning, notification, manager state, integrity evidence, and recovery behavior.
  11. Force a background GPS upload to return an authentication failure, and separately exercise a controlled permanent rejection. Confirm the just-collected batch enters the original organization/user's queue before the interruption event and native tracking suspension, then verify retry recovery or Needs review behavior as appropriate.
  12. With GPS or another canvassing write still pending, sign out, change users, or switch organizations during an active shift. Confirm native tracking stops immediately, downloaded canvassing views for the departed scope are cleared, the scoped GPS work is not deleted, and the shift end is queued behind the earlier work when the flush is pending or blocked by authentication. Confirm another account or organization cannot read or process that queue, then return as the original account in the original organization and verify the queue can recover in dependency order.
  13. Capture, sync, view, and delete an estimate photo. Confirm customer and PDF

reads use expiring signed access, a queued deletion cancels the upload, and deleting a synced photo removes the private stored object without exposing another estimate or client.

  1. Inspect the installed build and backup behavior to confirm Android and iOS sensitive-storage exclusions survived native generation and signing. Confirm the Android manifest does not request microphone access.

Record the device, OS version, build profile, build identifier, permission states, timestamps, and observed server records. Simulator and automated tests do not replace this evidence.

8. Pilot and Enablement

Start with a small internal territory and controlled recipients.

  • Review the employee location notice and raw GPS retention setting.
  • Confirm leaderboard privacy and private coaching access.
  • Validate disposition requirements and quick estimate templates.
  • Confirm manager escalation for failed offline items.
  • Review recovered-visit communication suppression and every automatic integrity-signal category with managers.
  • Reconcile visits, appointments, estimates, signatures, jobs, revenue, commission, and XP attribution.
  • Review API, Edge Function, geocoding, AI, email, SMS, and rate-limit logs.

After the pilot passes, the organization owner can open Canvassing and select Enable Canvassing.

Retained V1 Canvassing Final Release Gates

These gates apply to the retained V1 native canvassing release, not the V2 web-only property-loading slice described below. Do not describe the V1 native release as live until all of the following evidence is attached to its release record:

  • Real production secrets and restricted signing/key configuration have been verified through their actual paths.
  • GET /api/health/canvassing returns 200 in production; an authorized

release-monitor request reports no missing schema paths.

  • The complete migration chain has applied successfully to an upgraded production-snapshot PostgreSQL/PostGIS staging database, with RLS and service-role boundaries tested.
  • The production-like load/concurrency test meets its predeclared thresholds.
  • Signed Android and iOS physical-device tests cover online, offline, locked/background GPS, retries, stale recovery, scope changes, and backup exclusions.
  • Privacy/labor counsel or another authorized reviewer has approved the notices and retention choices.
  • App Store and Google Play credentials, disclosures, background-location review, and required approvals are complete.

Current V2 Property-Loading and CSV Import Release Status

The manager property-loading and CSV import releases are limited to web and trusted backend work. Release them migration-first: apply the complete journal through 0078_green_hellfire_club, including 0068, 0074, 0075, 0076, the unrelated intervening 0077, and 0078, then verify the resulting private schema and active scope before promoting the web/API feature. The exact feature head must then pass the full build, migration drift check, PowerSync exclusion sentinel, tenant/office/ assignment authorization suites, PostgreSQL duplicate races, bounded-query plans, and protected deployment smoke.

Every property-loading service must enter as app_user with the exact organization and member request settings. The temporary owner read used for the 0074 rolling-deploy window is allowed only after that normal-role scope check; the transaction restores app_user and reauthorizes the same territory before returning rows. Service-role callers cannot manufacture access by copying request settings.

Canvassing writers share the row/advisory order: lock the actor and relevant assignee members in deterministic order, take the organization canvassing gate, then lock and re-read the campaign or territory. Identity-provider offboarding takes its existing Calendar capacity gates first, then follows the same member-to-canvassing-to-territory order. Manual target adds, assignment replacement, and member offboarding race tests must cover both commit orders. Private 0076 CSV confirmation already enters through this same order and must pass its cross-writer races before deployment. The manager-facing upload, preview, and confirmation workflow preserves the same ordering and repeats those races before release.

Production readiness for this slice proves only Campaigns & territories, manual map-point/address loading, eligible CRM property loading, and the bounded territory target list plus manager CSV upload, private preview, and explicit confirmation. It is not evidence for target movement, functional Field view Map/List modes, a native Canvass tab, visits, dispositions, GPS, reporting, competitions, XP, or AI. Keep all of those controls unavailable until their own bounded release and, for visible mobile work, explicit product approval.

Retained V1 Canvassing Release Evidence

The remaining section records the earlier V1 canvassing release and launch procedure. Its Supabase migration counts, native build identifiers, readiness endpoint, and production aliases do not describe the current V2 deployment. Use them as parity and acceptance inputs only.

The July 24 production database rollout is complete. Before the write, the release operator captured the public, auth, storage, and supabase_migrations schemas plus every table's production rows in a hashed recovery manifest. The exact 46-migration backlog then passed twice against the live PostgreSQL 17 schema and data inside rollback-only transactions, including the migration-ledger writes, before the same transaction body committed.

Post-apply evidence now shows:

  • The Supabase migration ledger is 224/224 through

20260723269000_service_role_runtime_grants.sql.

  • The protected production readiness check sees all 44/44 required

relations and RPCs, with zero missing paths.

  • All 19 canvassing tables have RLS enabled.
  • All 74 canvassing/mobile proposal routines retain service_role execution,

and every canvassing table retains the required trusted-server DML grants.

  • A database-owner-free smoke using the authenticated role and a real active

organization member can read the campaign relation through RLS.

  • No temporary backup or diagnostic role remains in production.

Vercel production currently contains the encrypted legacy GOOGLE_MAPS_SERVER_KEY, while GOOGLE_MAPS_SERVER_API_KEY is not yet present. The import service accepts that legacy server-only alias temporarily and always prefers the canonical variable when both exist. Because encrypted Vercel values cannot be exported or copied through the CLI, rotate the credential into GOOGLE_MAPS_SERVER_API_KEY, verify server geocoding, and then remove the legacy alias.

The compatibility alias removes the immediate missing-variable blocker, but it does not prove the final Maps restriction model. Before production enablement, verify the legacy value is a separately restricted server credential, complete the canonical-name rotation, run the authorized readiness request until it returns 200, and complete the remaining native, load, physical-device, store, and legal gates above.

Supabase's passwordless CLI migration-list command still fails while the platform initializes its PostgreSQL 17 cli_login_postgres role. This does not affect Auth, PostgREST, RLS, service-role APIs, or the application runtime. Use the protected readiness endpoint and the migration ledger for release evidence until Supabase repairs that platform-side CLI route; do not add a persistent diagnostic login role as a workaround.

Command-center setup message

If the command center reports Canvassing setup incomplete, the application has detected that a required canvassing table, column, or RPC is not available in the connected Supabase schema. This is a deployment configuration problem, not a campaign permission denial. The command center stays offline, returns HTTP 503 with code CANVASSING_SETUP_INCOMPLETE, and does not retry queued native writes or change campaign data.

Release operators should:

  1. Keep canvassing disabled for customer organizations.
  2. Run the protected GET /api/health/canvassing check and record every

missing schema path.

  1. Apply and validate the complete migration chain in the production-snapshot

staging environment before production.

  1. Confirm the protected readiness response is 200 before enabling the

command center or issuing new signed native builds.

For the current July 24 rollout, steps 2 and the production database portion of step 3 are complete: the monitor reports zero missing schema paths and the production ledger is current. Step 4 remains blocked only by the private server geocoding key and the other non-database release gates above.

Do not troubleshoot this message by broadening organization permissions or assigning more territories. Those changes cannot repair a missing database contract.

Documentation may be published before the application rollout so the launch team can follow this checklist. Publication of this page is not a claim that the production migration, web deployment, EAS build, App Store release, Google Play release, legal/store approvals, load test, or physical-device acceptance has occurred.

Was this article helpful?

Still need help? Contact support