SOURCE OF TRUTH
Source: huint_supabase/migrations/20260820100000_baseline_prod_v2.sql (single migration, current baseline, no forward migrations on top of it), cross-checked against Huint_mcp/src/createTask.ts and the RPC bodies it calls. Columns that exist in the DB but nothing writes today — photo_notes, additional_details, priority, is_test, photo_guidance_url, webhook_*, metadata, location_display_snapshot, submissions.ip_lat/ip_lng — are cut. Each was checked against every read/write across the whole migration.
MCP input? — yes an agent sets it via create_photo_task/quote_task · derived = set automatically from another input · computed = system-owned, never agent input.
01 / TASKS
IDENTITY & OWNERSHIP
id
uuid
computed
PK
operator_id
uuid
derived
from the authenticated bearer
api_key_id
uuid
derived
which API key authenticated the create, if API-key auth
agent_id
text
derived
from X-Huint-Agent-Id header or key/JWT-derived default
agent_context
jsonb
derived
always {“agent_id”: “<agent_id>“}
agent_task_id
text
YES
agent’s own idempotency key, 1–200 chars, required
CONTENT / INSTRUCTIONS
title
text, ≤80 chars
YES
optional; auto-derived from tasker_instructions’ first sentence when omitted
instructions
text, ≤700 chars
YES
via tasker_instructions — shown verbatim to the tasker; renamed at the MCP→RPC boundary
dont_do
text
YES
DB allows ≤500 chars; the MCP tool itself caps input at 120 chars today (tighter, UI-driven)
media_type
text
computed
1 live value: photo
required_media
integer, 1–10, default 1
YES
legacy aliases required_photos/photo_count are silently mapped onto this
task_type
text
computed
1 live value: picture_required (the default) — drives the map-pin icon / capture flow
LOCATION & REACH
location_mode
text
YES
via location.mode — 4 live values: anywhere, fixed_location, city, zip_code
location
geography(Point,4326)
derived
set only when location_mode = fixed_location
radius_meters
integer, 30–2000, default 100
YES
via location.radius_meters — fixed_location only
location_scope
jsonb
derived
Apple-resolved scope object for city/zip_code (shape in The Sub-Shapes)
friendly_location_name
text
derived
resolved place name, fixed_location only
formatted_address
text
derived
resolved address, fixed_location only
google_place_id
text
derived
set only when Google was the fallback geocoder
location_provider
text
derived
2 live values: apple, google
provider_place_id
text
derived
provider-native place id
apple_map_item_identifier
text
derived
MapKit item id, when Apple returns one
location_resolution_confidence
numeric 0–1
derived
v0 resolvers always write 1.0
delivery_mode
text
derived
2 live values: location (iff fixed_location), else global
presence_required
boolean, default true
derived
true iff location_mode = fixed_location
place_search_query
text, 2–120 chars
YES
via place_selection.place_search_query — phone searches this near the tasker after claim; binds the entry to an Apple place
ECONOMICS
bounty_cents
integer, ≥100
YES
$1.00 floor
platform_fee_cents
integer
computed
round(bounty_cents * 0.10)
prize_cents
integer, ≥0, default 0
YES
operator-managed display-only grand prize; never escrowed, excluded from the hold, never shown to a tasker as expected earnings
SLOTS / CONTEST
max_taskers
integer, 1–1000, default 1
YES
a contest is max_taskers > 1 on the same object, not a separate type
verified_photo_count
integer, default 0
computed
incremented by the verify path as photos verify; confirmed live (not vestigial)
ELIGIBILITY GATES & ENTRY QUESTIONS
requires_x_account
boolean, default false
YES
optional; 2 values: true/false
eligible_country_code
text
YES
optional; see Two Fields, No Curated List below — not a curated list
entry_questions
jsonb array, default []
YES
max 10 questions; shape in The Sub-Shapes
LIFECYCLE / STATUS & BOOKKEEPING
status
enum
computed
7 live values, see Enums below
deadline
timestamptz
derived
from expires_in_hours: now() + hours; 1h floor, 30-day cap; omitted → no auto-expiry
claimed_by / claimed_at
uuid / timestamptz
computed
set on claim
claim_expires_at
timestamptz
computed
claimed_at + 30 min
submitted_at
timestamptz
computed
starts the 15-min agent review window
verification_deadline
timestamptz
computed
submitted_at + 15 min, flat for every task regardless of bounty (one function, no args, despite a stale column comment implying otherwise)
completed_at / cancelled_at
timestamptz
computed
terminal timestamps
created_at / updated_at
timestamptz
computed
bookkeeping
02 / TASK_CLAIMS
id, task_id, tasker_id
uuid
PK / FKs
status
text
7 live values, see Enums below
claimed_at
timestamptz
—
expires_at
timestamptz
claimed_at + 30 min
submitted_at / completed_at / abandoned_at / expired_at / rejected_at
timestamptz
terminal-state timestamps, at most one populated
rejection_reason
text
free-text, mirrored from submissions.rejection_reason at verify time
rejection_reason_code
enum
mirrors the submission’s code, see Enums below
rejection_notes
text, ≤280 chars
—
payout_released
boolean, default false
CHECK: can only be true when status = ‘verified’
payout_released_at
timestamptz
—
device_hash
text
fingerprint captured at claim time; every submission’s device_hash must match
attest_key_id
text
Apple App Attest key id active at claim time
notified_expiring_soon_at
timestamptz
claim-expiry-warning push dedup
location_verification
jsonb
Apple structured-address match evidence captured at claim
created_at
timestamptz
—
03 / SUBMISSIONS
One row per photo (media_index 1–10 within a claim).
id, task_id, tasker_id, claim_id
uuid
PK / FKs
media_index
integer, 1–10, default 1
which required photo this is
media_path / media_url
text
storage path / served URL
mime_type
text, default image/jpeg
—
file_size_bytes
integer
—
capture_lat / capture_lng / capture_heading / altitude_meters / accuracy_meters
double precision
device-reported capture geometry, written from the submitted media payload
capture_timestamp
timestamptz, not null
—
device_hash
text, not null
must equal the claim’s device_hash
gps_verified
boolean
—
gps_distance_meters
double precision
distance from task location
timestamp_verified / hash_verified
boolean
—
location_verification
jsonb
Apple structured-address evidence
vision_checked / vision_checked_at
boolean / timestamptz
whether the Vision Gate AI pre-check ran
vision_confidence
double precision, 0–1
—
vision_result
jsonb
raw AI check output
status
enum
3 live values, see Enums below
rejection_reason
text
free-text, written by the verifier (Vision Gate or operator agent)
rejection_reason_code
enum
the 5-code closed set; GPS/device/timestamp/photo-count hard-fails never land here
rejection_notes
text, ≤280 chars
operator-agent free note on reject
is_flagged
boolean, default false
—
reviewed_by / reviewed_at
uuid / timestamptz
human reviewer, when applicable
verified_at / created_at
timestamptz
—
04 / TASK_ENTRIES
Only populated when a task carries entry_questions and/or place_selection. Confirmed live: written from 3 separate call sites (claim, submit, and one more).
id, task_id, tasker_id, claim_id, submission_id
uuid
PK / FKs; submission_id null while entry_state = prepared
entry_state
text, default prepared
3 live values: prepared, submitted, verified
answers
jsonb object, default {}
keyed by entry_questions[].id
selected_place_provider
text
2 live values: apple, google — or all 5 place columns null together
selected_place_provider_id / selected_place_name / selected_place_address
text
—
selected_place_point
geography(Point,4326)
—
capture_to_place_max_distance_meters
double precision
the 804.672 m (0.5 mi) gate when place_selection is set
accuracy_result / area_result
jsonb
verification-outcome snapshots
x_user_id_snapshot / x_handle_snapshot
text
only when requires_x_account
consent_version_snapshot / consent_at_snapshot
text / timestamptz
—
location_verification
jsonb
—
prepared_at / submitted_at / verified_at / created_at / updated_at
timestamptz
—
05 / ENTRY_QUESTIONS · 06 / LOCATION
ENTRY_QUESTIONS — MAX 10 ITEMS, DISCRIMINATED ON TYPE
single_select / multi_select
{ id, prompt (≤200 chars), type, required, options: [{ value, label (≤120 chars) }, 1–20 items, unique values] }
short_text
{ id, prompt (≤200 chars), type, required }
id pattern: ^[A-Za-z][A-Za-z0-9_-]{0,63}$, unique within the array.
CREATE-TIME LOCATION INPUT — DISCRIMINATED ON MODE
anywhere
no fields
fixed_location
{ location, radius_meters (30–2000, default 100) } — location is either a simple form (address and/or place_name and/or lat/lng, resolved server-side) or a pre-resolved form (full payload from a prior resolve_location call)
city
{ city (1–120), state_code, country_code }
zip_code
{ zip_code (5-digit), country_code }
STORED / NORMALIZED
fixed_location stores a full resolved payload — { lat, lng, friendly_location_name, formatted_address, location_provider, provider_place_id, apple_map_item_identifier?, location_resolution_confidence, google_place_id? }. city/zip_code store an Apple scope object instead:
location_scope: { schema_version: 1, provider: “apple”, provider_place_id, country_code, display_name, map_label, center: {lat,lng}, display_region: {south_latitude, west_longitude, north_latitude, east_longitude}, type, state_code, city }
(or zip_code in place of city for the zip variant). Immutable once resolved — quote/create resolve city/ZIP input to this before activation.
COUNTRY CODE, IN FULL
01
Location country_code (fixed_location’s address input isn’t affected; only city/zip_code modes take this) — hard-locked to exactly one live value: “US”. The Zod schema literal-matches US only; nothing else is accepted. This is a true closed set of one, not a placeholder — Huint’s location resolution only covers the US today.
02
eligible_country_code (the task’s tasker-eligibility gate) — not a curated list. Both this field and the tasker’s own taskers.country_code are format-checked only (^[A-Z]{2}$, uppercase ASCII letters) at the database level; eligibility is decided by simple string equality between the two (private.tasker_eligibility_verdict). An operator can type any syntactically-valid 2-letter code (e.g. CA, GB) and it will be stored and matched — there’s no server-side check against a real ISO-3166 list or against where Huint actually operates. If the iOS app’s own country picker restricts what a tasker can set on their profile, that would be the only real-world limiter, and it lives outside this backend/MCP surface.
Related: state_code in the location-scope input is likewise format-checked only (^[A-Z]{2}$) — real validation happens implicitly when Apple’s geocoder fails to resolve a bogus state.
07 / ENUMS · 08 / TASK_BLOCKLIST
ENUMS
task_status (7)
open, claimed, submitted, verified, rejected, expired, cancelled
task_claims.status (7)
active, submitted, verified, rejected, abandoned, expired, released_device_change
submission_status (3)
pending, verified, rejected
submission_rejection_reason (5)
unclear, wrong_subject, framing_or_angle, suspected_reuse, policy_violation — agent-judgment only; GPS/device/timestamp/photo-count fail automatically and never touch this enum
task_blocklist.reason (5)
held_too_long, hand_abandoned, rejected, reported, admin_block
TASK_BLOCKLIST — WHO CAN’T RE-CLAIM A GIVEN TASK
task_id / tasker_id
uuid
composite scope
reason
text
5 live values, in the enums table above
claim_id
uuid
the claim that caused the block, if any
created_at / acknowledged_at
timestamptz
—
A reject blocks one tasker from one task — it doesn’t touch their account standing anywhere else.
The full map, print-ready.
Huint is a product of Nanu Connect LLC, doing business as Huint Labs. Companion to the Huint MCP Reference. Prepared August 2026.
