Offer Invitation & Digital Offer Submission
Offer Invitation & Digital Offer Submission
The offer invitation workflow allows agents to send a prospective tenant a secure, branded link to submit a rental offer online. No tenant account or login is required.
Overview
The workflow has three stages:
- Agent sends an invitation — the agent creates a tokenised invitation link tied to a specific property and dispatches a branded email to the prospective tenant.
- Tenant submits an offer — the tenant follows the link to a public form, fills in their details, and submits.
- Automatic processing — on submission, the offer transitions to
with_agent, applicant records are created, and the agent is notified by email.
Stage 1: Sending an Invitation
An agent sends an invitation using the offer.sendInvitation mutation.
What happens internally:
- A unique token (
UUID-shortUUID, 44 chars) is generated and stored in theoffer_invitation_tokenstable. - The agency's branding (logo URL, primary colour, name) is denormalised onto the token record so the public form can render without making authenticated queries.
- An Inngest event is dispatched to send the branded invitation email.
Token properties:
| Property | Value |
|---|---|
| Format | UUID-shortUUID (44 chars, URL-safe) |
| Expiry | 14 days from creation |
| Single-use | Yes — marked isUsed on first submission |
Stage 2: The Public Offer Form
The tenant receives an email and follows the link to /offer/[token].
The form is publicly accessible (no login required) and renders the agency's branding from the token. The form collects the following information:
Personal Details
| Field | Required |
|---|---|
| First name | ✅ |
| Last name | ✅ |
| Phone | Optional |
| Date of birth | Optional |
| Current address | Optional |
Employment
| Field | Required |
|---|---|
| Employment status | ✅ (employed, self-employed, unemployed, student, retired, other) |
| Employer name | Optional |
| Annual income | Optional |
Household Details
| Field | Notes |
|---|---|
| Pets | Toggle + free-text details |
| Adverse credit history | Toggle + free-text details |
| Smoker | Toggle |
Joint Applicants
- Up to 5 joint applicants can be added.
- Each joint applicant provides: first name, last name, email, phone, and relationship to the primary applicant.
- Joint applicants with a first name, last name, and email are saved as
applicantsrows withrole: "joint".
Offer Terms
| Field | Notes |
|---|---|
| Proposed monthly rent | Optional |
| Proposed move-in date | Optional |
| Additional notes | Free text, for negotiation details |
GDPR
A consent notice is displayed on the form. Data is stored with the orgId for tenant isolation.
Stage 3: Submission Processing
When the tenant submits the form, the following happens automatically:
- Token validation — the token is checked for expiry and
isUsedstatus. Invalid or expired tokens return an error. - Offer status transition — the offer moves from
invited→with_agentin one step. - Applicant records created — the primary applicant and any valid joint applicants are saved to the database.
- Agent notification — the agent receives an email with a direct link to the submitted offer, dispatched via Inngest.
- Audit log — entries are written for both the invitation creation and the offer submission.
- Token marked used — the token's
isUsedflag is set totrue, preventing re-submission.
Error States
The public form handles the following error conditions gracefully:
| Condition | Displayed message |
|---|---|
| Token not found | "This offer invitation link is invalid or has expired." |
| Token expired (>14 days) | Same as above |
| Token already used | Same as above |
| Network/server error | Error message from the server response |
Technical Reference
tRPC Procedures
offer.sendInvitation (authenticated mutation)
- Creates an invitation token and dispatches the invitation email via Inngest.
offerInvite.getSession (public query, token-authenticated)
- Validates the token and returns agency branding + property address for form rendering.
offerInvite.submitOffer (public mutation, token-authenticated)
- Accepts the full form payload, creates applicant records, transitions offer status, and triggers agent notification.
Inngest Functions
| Function ID | Trigger | Action |
|---|---|---|
offer-invitation-email | Invitation created | Sends branded invitation email to the prospective tenant |
offer-submitted-agent-notify | Offer submitted | Sends notification email to the agent with a link to the offer |
Database Table: offer_invitation_tokens
| Column | Description |
|---|---|
token | The unique invitation token (primary key) |
offerId | Foreign key to the associated offer |
expiresAt | Timestamp 14 days after creation |
isUsed | Boolean flag, set to true after submission |
| Branding fields | Denormalised agency name, logo URL, primary colour |
Middleware
The /offer path is added to public routes in src/middleware.ts so the offer form is accessible without an authenticated session.