Application Reference
An Application Reference is the human-readable identifier for an intake-form submission. Staff Portal shows it as Application Reference. Operators and applicants use it to find a draft or submitted form. APIs and the database still use submission_id (a UUID) as the primary key.
It is not a Functional ID. A Functional ID (functional_record_id) is assigned later, after the submission is approved and ingested into a Register. An Application Reference exists from the moment the intake submission row is created.
Identifiers compared
submission_id
On create (UUID)
Intake submission (PK) and section rows
APIs, workers, internal joins
application_reference
On create (format string)
Intake submission (unique) and copied onto section rows
Staff Portal list, breadcrumbs, search/display names
functional_record_id
After ingest into a Register
Live register rows
Real-world registry ID (Farmer ID, Household ID, …)
When it is generated
G2PIntakeFormSubmission.application_reference has a SQLAlchemy default that calls generate_application_reference(). The value is written on insert - typically the first save of a new intake form, while the submission is still DRAFT.
The generator is constructed once during core initialization:
generate_application_reference(now: datetime | None = None) -> str then uses that singleton. If no generator is registered (tests / isolated core), it compiles Settings.application_reference_format on the fly.
The same value is stamped onto each intake section row (G2PIntakeForm.application_reference) when the section is saved, so list/search helpers can read it from the row payload. After ingest, live register rows do not store application_reference.
Configuration
The setting is application_reference_format on registry core Settings (env_prefix="registry_core_"). Each API subclass inherits the field but uses its own env prefix. Intake submissions are created by those APIs, and each process registers its Settings before core initialization.
Process
Env var
Helm envVars key
Staff Portal API
REGISTRY_STAFF_PORTAL_API_APPLICATION_REFERENCE_FORMAT
staffApi.envVars
Partner API
REGISTRY_PARTNER_API_APPLICATION_REFERENCE_FORMAT
partnerApi.envVars
Beneficiary Portal API
REGISTRY_BENE_PORTAL_API_APPLICATION_REFERENCE_FORMAT
beneApi.envVars
Core Settings in isolation (local / tests)
REGISTRY_CORE_APPLICATION_REFERENCE_FORMAT
-
Pydantic Settings treats env names as case-insensitive. Quotes around the value are required in shells when the format contains { } or |.
Helm example (Staff Portal API):
Set the same format on every API that can create submissions. Otherwise Staff Portal drafts, Partner-created submissions, and Beneficiary Portal submissions will look like different numbering schemes.
The format is compiled and test-rendered at startup. A bad format raises ApplicationReferenceFormatError and the process does not come up.
Format syntax
A format string is literals plus tokens.
Literals - any text outside
{...}is copied exactly (APP-,-,/).Tokens -
{TOKEN},{TOKEN:argument}, or{TOKEN:argument|upper}.
Token names are case-insensitive. The only modifier is |upper, and it is applied to DATE and TIME segments (month/weekday names from strftime). Other tokens ignore |upper. {UUID8} is already uppercase hex.
Token reference
DATE
{DATE:<strftime>}
datetime.strftime of the generation time
Requires a valid Python strftime pattern
TIME
{TIME:<strftime>}
Same as DATE; use for clock-time segments
Requires a valid Python strftime pattern
SECONDS
{SECONDS:<width>}
Zero-padded seconds since midnight (0–86399)
Width 1–5
EPOCH
{EPOCH:<width>}
Zero-padded Unix epoch seconds
Width 1–12. Width is minimum padding, not truncation - a 10-digit epoch will still render 10 digits if you pass {EPOCH:5}
RAND
{RAND:<width>}
Zero-padded decimal digits (0–9), from secrets.randbelow
Width 1–12
RAND_ALNUM
{RAND_ALNUM:<width>}
Random A–Z and 0–9
Width 1–12
UUID8
{UUID8}
First 8 hex characters of a UUID4, uppercased
No argument
Unknown tokens, empty format strings, missing widths, and invalid strftime patterns are rejected at compile time.
DATE / TIME strftime patterns
DATE and TIME accept any valid Python strftime pattern. The compiler checks the pattern against a fixed timestamp (2001-01-01 12:30:45).
%Y%m%d
20260821
Compact ISO-style date
%Y%b%d|upper
2026AUG21
Year + uppercase abbreviated month + day (default date segment)
%d%m%Y
21082026
Day-first numeric date
%d%b%Y
21Aug2026
Day + abbreviated month + year
%d%b%Y|upper
21AUG2026
Uppercase month via |upper
%d%B%Y
21August2026
Full month name (length varies by month)
%Y/%m/%d
2026/08/21
Slashes inside the token; or put them in literals
%H%M%S
143025
24-hour time
%H:%M:%S
14:30:25
Time with colons
Month and weekday names follow the process locale (usually English in container images).
Example formats
Default
The last six characters are five-digit seconds-since-midnight plus one random digit. Fine for low concurrent traffic; see Uniqueness before using this in production.
ISO date, seconds, and a wider random segment (recommended for volume)
Day-first date with time
Agency prefix and uppercase month
Slashes in literals
Alphanumeric random segment
UUID fragment
Limits and validation
Maximum rendered length
64 characters (checked at compile using an estimated max, and again at render)
{RAND} / {RAND_ALNUM} width
1–12
{SECONDS} width
1–5
{EPOCH} width
1–12
Empty format
Rejected
Unknown token
Rejected
Invalid strftime
Rejected
{UUID8} with an argument
Rejected
Modifier other than upper
Rejected
Uniqueness
g2p_intake_form_submissions.application_reference has a unique constraint. There is no retry if two concurrent inserts collide.
Entropy comes from the time tokens plus {RAND}, {RAND_ALNUM}, or {UUID8}. {SECONDS:5} distinguishes submissions in different seconds of the same day. Within the same second you need enough random width:
{RAND:1} (default)
10
{RAND:5}
100,000
{RAND_ALNUM:6}
36⁶
{UUID8}
16⁸ (hex)
The default {RAND:1} is a collision risk if many submissions are created in the same second. For production, use at least {RAND:5} or {RAND_ALNUM:6} in addition to a date (and optionally seconds) segment.
Display names
The base domain service appends application_reference to the intake record_name when it is not already present:
Override construct_intake_record_name when the intake list should lead with the reference (common on master registers). Include application_reference in construct_search_text if staff will type it into search.
Live register construct_record_name should not depend on application_reference - that column is not on register tables.
Signatures (code)
compile is what validates the implementer-supplied format at startup. generate renders it for the current time (datetime.now() unless now is passed).
Last updated
Was this helpful?