ARFC-1006: Predefined Link Actions
Status
| Implemented | Draft Date: 2026-05-16 | Last Call Date: 2026-05-23 | Publication Date: 2026-05-30 | Version: 0.4 |
Abstract
This RFC proposes a LinkActions subsystem for controlled retrieval of server-stored client instruction sets through opaque finite-use tokens. A LinkAction is not tied to email and may be distributed through any mechanism, including frontend URLs, QR codes, chat systems, or operator copy-paste. Retrieval is a consumptive backend operation that validates lifecycle and authentication requirements, decrements a remaining-use counter, records a successful retrieval, and returns declarative client instructions in the standard response envelope.
This RFC also defines how invitation-style email delivery fits into that model. Invitation emails are not a separate standalone subsystem. Instead, invitation-style delivery uses the existing notification system, extended to support external email recipients and to render link-action URLs into email templates.
Table of Contents
- Introduction
- Motivation
- Specification
- 3.1 Design Principles
- 3.2 Core Concepts
- 3.3 Data Model
- 3.4 Token Model
- 3.5 Authentication Requirements
- 3.6 Lifecycle and Finite Use Model
- 3.7 Retrieval Flow
- 3.8 API Endpoints
- 3.9 Client Instruction Grammar
- 3.10 Client Implementation Contract
- 3.11 Retrieval Failure Contract
- 3.12 Validation Rules
- 3.13 Stored Procedures
- 3.14 External Email Distribution via Notifications
- Open Questions
- Security Considerations
- Backward Compatibility
- References
- Author
1. Introduction
The platform needs a reusable mechanism for presenting users with predefined client-side next steps after following a link. These links may originate from invitation emails, QR codes, chat messages, copied URLs, or other delivery mechanisms. The current platform does not provide a unified backend model for managing such links, enforcing lifecycle and authentication requirements, or returning a bounded declarative instruction set to the frontend.
This RFC introduces LinkActions as an orthogonal backend subsystem. A LinkAction stores server-side client instructions and exposes them through a consumptive retrieval endpoint. The backend validates whether the link is still usable and whether the current principal is permitted to retrieve it. If valid, the backend records the retrieval, decrements the remaining-use count, and returns the instruction set for frontend dispatch.
This RFC intentionally limits LinkActions to retrieval of declarative client instructions. It does not define a backend action executor model. When a workflow needs invitation-style email delivery, that delivery is handled by the existing notification pipeline rather than by a separate invitation-email persistence model.
2. Motivation
Current State
- Several platform flows need link-based entrypoints that survive out-of-band distribution
- Existing invitation and notification flows are delivery-specific
- There is no reusable server-side model for finite-use links with authentication requirements
- The platform does not yet have a unified way to return declarative post-link instructions to the frontend
- External-recipient email delivery now needs to coexist with link-based entrypoints without creating a second action-execution mechanism
Missing Capabilities
- Opaque tokens that retrieve server-stored client instructions
- Authentication requirements attached to a link as a whole
- Finite-use link semantics with a remaining-use countdown
- Explicit lifecycle control for revocation and expiry
- Successful consumptive retrieval audit history
- Multi-option instruction payloads that the frontend can process locally
Design Goals
- Make link actions independent from any delivery mechanism
- Keep all authoritative semantics on the server
- Support unauthenticated, authenticated-any-principal, and authenticated-specific-principal retrieval
- Support finite-use links with atomic usage decrement
- Return declarative client instructions rather than router-specific URLs or arbitrary JavaScript
- Keep the initial design simpler than a generalized backend action execution framework
- Reuse the notification system for invitation-style email delivery rather than defining a separate invitation-email subsystem
3. Specification
3.1 Design Principles
The LinkActions subsystem follows these principles:
- Orthogonal to Delivery: A
LinkActiondoes not require email and does not assume email. - Opaque Token: The public token is an unguessable bearer token stored server-side as an opaque value.
- Server-Stored Semantics: Authoritative lifecycle, authentication requirements, and client instructions live in the database.
- Consumptive Retrieval: Retrieval decrements remaining uses and is therefore a mutating operation.
- Declarative Client Instructions: The backend returns structured client instructions for a bounded frontend dispatcher.
- Finite Use Only: Every
LinkActionhas a finite remaining-use count.
3.2 Core Concepts
LinkAction
A LinkAction is one tokenized retrieval unit.
Each LinkAction has:
- one opaque public token
- one lifecycle state
- one authentication requirement
- one finite remaining-use count
- one server-stored
clientInstructionsJSONpayload
Retrieval of a LinkAction returns the instruction payload in the standard response envelope and consumes one use.
Consumptive Retrieval
Retrieval is the authoritative backend operation. The backend:
- validates the token and lifecycle
- validates the authentication requirement
- records a successful retrieval
- decrements
remainingUseCount - returns
clientInstructionsJSON
The backend does not observe which instruction option the client later selects.
3.3 Data Model
LinkActions
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
INT | NOT NULL | Auto-increment primary key |
externalID |
BINARY(16) | NOT NULL | Public identifier for administrative and API use |
internalUUID |
BINARY(16) | NOT NULL | Service-to-service identifier |
token |
CHAR(64) | NOT NULL | Opaque public token value |
authenticationRequirement |
ENUM | NOT NULL | unauthenticated, authenticated-any-principal, or authenticated-specific-principal |
requiredUserEID |
BINARY(16) | NULL | Required public user EID when authenticationRequirement = authenticated-specific-principal |
remainingUseCount |
INT | NOT NULL | Remaining number of successful consumptive retrievals allowed |
expiresAt |
DATETIME(6) | NOT NULL | Absolute expiration time |
status |
ENUM | NOT NULL | active, revoked, or expired |
clientInstructionsJSON |
JSON | NOT NULL | Declarative client instruction set |
createdByUserID |
INT | NOT NULL | Creator |
createdTimestamp |
DATETIME(6) | NOT NULL | Creation time |
lastModifiedByUserID |
INT | NOT NULL | Last modifier |
lastModifiedTimestamp |
DATETIME(6) | NOT NULL | Last modification time |
LinkActionRetrievals
This table records successful consumptive retrievals only.
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
BIGINT | NOT NULL | Auto-increment primary key |
externalID |
BINARY(16) | NOT NULL | Public identifier |
internalUUID |
BINARY(16) | NOT NULL | Service identifier |
linkActionID |
INT | NOT NULL | FK to LinkActions |
retrievedByUserEID |
BINARY(16) | NULL | Public user EID of the retriever if known |
remainingUseCountAfter |
INT | NOT NULL | Remaining uses after successful decrement |
retrievedTimestamp |
DATETIME(6) | NOT NULL | Retrieval time |
requestMetadataJSON |
JSON | NULL | Request metadata snapshot |
createdByUserID |
INT | NOT NULL | Actor if known, otherwise system actor |
createdTimestamp |
DATETIME(6) | NOT NULL | Creation time |
lastModifiedByUserID |
INT | NOT NULL | Last modifier |
lastModifiedTimestamp |
DATETIME(6) | NOT NULL | Last modification time |
3.4 Token Model
The public token is an opaque high-entropy bearer token. It is not signed and is not self-describing.
This RFC uses a simple token generation model:
rawToken := lowercaseHex(sha256(newUUID()))
The generated token value is stored directly and distributed externally.
newUUID() denotes a cryptographically random UUID. The SHA-256 digest is encoded as 64 lowercase hexadecimal characters.
The raw token MUST use:
- a fixed length
- a URL-safe alphabet
- a representation suitable for manual transcription and QR distribution
For version 1, the token format is a 64-character lowercase hexadecimal string:
^[a-f0-9]{64}$
The token MUST NOT contain:
- action semantics
- authentication metadata
- expiration metadata
- client instruction JSON
- redirect URLs
Lookup model:
linkAction := LinkActions.lookup(token)
3.5 Authentication Requirements
Authentication applies to the entire LinkAction retrieval, not to individual instructions inside clientInstructionsJSON.
Allowed values:
| Value | Meaning |
|---|---|
unauthenticated |
Retrieval does not require an authenticated platform user |
authenticated-any-principal |
Retrieval requires any authenticated platform user |
authenticated-specific-principal |
Retrieval requires the specific requiredUserEID |
Rules:
requiredUserEIDis required only forauthenticated-specific-principalrequiredUserEIDmust be null for the other two modes
3.6 Lifecycle and Finite Use Model
Lifecycle States
| State | Meaning |
|---|---|
active |
Link may be retrieved if other checks pass |
revoked |
Link is administratively disabled |
expired |
Link is no longer valid, including explicit early expiration |
Executable / Retrievable Condition
A LinkAction is retrievable only if all of the following are true:
status = active
remainingUseCount > 0
expiresAt > now()
authentication requirement satisfied
Finite Use Semantics
remainingUseCountstarts at a finite positive integer- each successful consumptive retrieval decrements it by 1
- when it reaches
0, the link is exhausted - exhaustion is represented by
remainingUseCount = 0, not by a separate lifecycle state - time expiry is enforced dynamically from
expiresAt
3.7 Retrieval Flow
flowchart TB
Start["Frontend has raw token"] --> Retrieve["POST retrieve link action"]
Retrieve --> Lookup["Resolve token to LinkAction"]
Lookup --> CheckState{"status = active?"}
CheckState -->|No| RejectState["Reject"]
CheckState -->|Yes| CheckExpiry{"expiresAt > now()?"}
CheckExpiry -->|No| RejectExpiry["Reject"]
CheckExpiry -->|Yes| CheckUses{"remainingUseCount > 0?"}
CheckUses -->|No| RejectUses["Reject"]
CheckUses -->|Yes| CheckAuth{"Authentication requirement satisfied?"}
CheckAuth -->|No| RejectAuth["Reject"]
CheckAuth -->|Yes| Consume["Insert retrieval row and decrement remainingUseCount atomically"]
Consume --> Return["Return response envelope with data.clientInstructions"]
Important Consequences
- Retrieval is the consumptive act.
- The backend does not observe which client instruction option is chosen later.
- Retrieval failures do not decrement usage and do not create retrieval rows.
- The frontend is responsible for dispatching the returned instructions locally.
3.8 API Endpoints
Public Retrieval Entry
This RFC assumes the user-facing link may point to a frontend URL such as:
https://www.codermerlin.academy/narp/app?linkAction={rawToken}
The frontend then performs the authoritative backend retrieval.
Backend Retrieval Endpoint
| Method | Path | operationId | Description |
|---|---|---|---|
| POST | /v2/link-actions/{token}/retrieve |
retrieveLinkActionV2 |
Perform a consumptive retrieval of a LinkAction |
This iteration does not define a separate non-consumptive administrative preview endpoint.
Retrieval Response
The endpoint returns the standard response envelope with the instruction set in data.clientInstructions.
The response does not include remainingUseCountAfter.
The response MUST include Cache-Control: no-store.
Conceptual response shape:
{
"envelope": {
"requestID": "...",
"status": "success"
},
"data": {
"clientInstructions": {
"version": 1,
"options": [
{
"key": "accept",
"label": "Accept",
"description": "Accept this invitation.",
"sequence": [
{
"action": "acceptInvitationFromTeam",
"parameters": {
"teamEID": "3c695ec5-0df8-4e43-9f35-312f0ab87f65"
}
}
]
},
{
"key": "decline",
"label": "Decline",
"description": "Decline this invitation.",
"sequence": [
{
"action": "declineInvitationFromTeam",
"parameters": {
"teamEID": "3c695ec5-0df8-4e43-9f35-312f0ab87f65"
}
}
]
}
]
}
}
}
3.9 Client Instruction Grammar
clientInstructionsJSON is declarative JSON consumed by a bounded frontend dispatcher.
The backend must not return arbitrary JavaScript or router-coupled implementation details.
Top-Level Shape
This RFC adopts a multi-option instruction-set grammar with a defined top-level structure and flexible content below that top level.
Conceptual shape:
{
"version": 1,
"options": [
{
"key": "accept",
"label": "Accept",
"description": "Accept this invitation.",
"sequence": [
{
"action": "acceptInvitationFromTeam",
"parameters": {
"teamEID": "..."
}
}
]
}
]
}
Option Fields
| Field | Type | Required | Description |
|---|---|---|---|
key |
string | Yes | Stable option identifier |
label |
string | Yes | Client-facing label |
description |
string | Yes | Client-facing description |
sequence |
array | Yes | Ordered list of dispatcher actions |
Option keys MUST be unique within a clientInstructionsJSON.options array.
Sequence Step Fields
| Field | Type | Required | Description |
|---|---|---|---|
action |
string | Yes | Registered frontend dispatcher action |
parameters |
object | No | Schema-validated parameter object |
Top-Level Fields
| Field | Type | Required | Description |
|---|---|---|---|
version |
integer | Yes | Instruction grammar version |
options |
array | Yes | Available client-dispatch options |
This RFC defines version = 1. Clients MUST reject unsupported versions.
Dispatcher Semantics
actionvalues come from a bounded frontend dispatcher registry- each dispatcher action has a defined parameter schema
- the backend validates the grammar and registered action schemas before storing the JSON
- the frontend must validate
version, option shape, and dispatcher action availability before presenting or executing options - dispatcher actions are semantic commands, not route names, URLs, or executable script fragments
Authentication reminder:
- for authenticated link actions, the backend has already enforced auth before returning the instruction set
- for unauthenticated link actions, the returned sequence may still include a client action such as
authenticateUser
3.10 Client Implementation Contract
Clients implement LinkActions through a frontend entry route and the consumptive retrieval endpoint.
Frontend Entry Route
A user-facing link SHOULD carry the token as an opaque query parameter:
https://www.codermerlin.academy/narp/app?linkAction={rawToken}
The client MUST treat the token as an opaque bearer value. It MUST NOT attempt to decode semantics from it.
After successful retrieval, the client SHOULD remove the raw token from the visible browser URL, for example by replacing the current history entry. The client SHOULD keep the retrieved instruction set only in transient application state needed to complete the local dispatch flow.
Retrieval Request
The client retrieves instructions with:
POST /v2/link-actions/{token}/retrieve
Request rules:
- if the current client has a bearer token, it SHOULD include the normal
Authorizationheader - if the user is not authenticated, it MAY call the endpoint without
Authorization - the client MUST NOT call this endpoint speculatively
- the client MUST guard against duplicate retrieval calls for the same page load
- the client MUST NOT automatically retry the request after transport failure without user action
- the client MUST NOT log the raw token to telemetry, analytics, or client-side error logs
The no automatic retry rule exists because retrieval may have succeeded server-side even if the response was lost.
Successful Retrieval Handling
On success, the client MUST:
- read
data.clientInstructions - verify
versionis supported - verify the instruction set shape is supported
- present available options to the user
- dispatch the selected option’s
sequencethrough the frontend dispatcher
The backend does not receive the selected option. Any server-side effects required by a selected option must be performed by normal API calls made by the relevant dispatcher action.
If the retrieved instruction set is unsupported by the client, the client MUST fail closed and present an unsupported-link-action state rather than attempting partial execution.
Dispatcher Requirements
The frontend dispatcher MUST:
- maintain a registry of supported semantic
actionnames - define a parameter schema for each action
- reject unknown action names
- reject invalid parameters
- execute sequence steps in order
- stop the sequence on the first failed step unless that action’s local contract explicitly defines different behavior
3.11 Retrieval Failure Contract
The retrieval endpoint returns the standard error envelope for failures. Failed retrievals do not decrement remainingUseCount and do not create LinkActionRetrievals rows.
| Condition | HTTP Status | Error Name | Client Behavior |
|---|---|---|---|
| Token does not match the v1 token format | 400 | InvalidRequest_ValueOfIncorrectFormat |
Show invalid-link state |
| Token is well-formed but not found | 404 | NotFound_LinkAction |
Show unavailable-link state |
Link action status is revoked |
410 | Gone_LinkAction_Revoked |
Show unavailable-link state |
Link action status is expired or expiresAt <= now() |
410 | Gone_LinkAction_Expired |
Show expired-link state |
remainingUseCount <= 0 |
410 | Gone_LinkAction_Exhausted |
Show already-used state |
| Authentication is required and absent or invalid | 401 | Unauthorized_AuthenticationRequired |
Start or prompt sign-in, then let the user retry retrieval |
Authenticated user does not match requiredUserEID |
403 | Forbidden_IncorrectPrincipal |
Show wrong-account state |
| Server cannot produce a valid instruction response | 500 | InternalServerError_LinkActionInstructionFailure |
Show generic failure state |
Clients SHOULD rely primarily on HTTP status and error name. Error message text is not a stable client contract.
Authentication failures are non-consumptive. After the user signs in, the client may retry retrieval with the same raw token.
3.12 Validation Rules
Creation Validation
IF remainingUseCount <= 0 THEN
REJECT 'INVALID_REMAINING_USE_COUNT'
IF expiresAt <= now() THEN
REJECT 'EXPIRY_IN_PAST'
IF authenticationRequirement = 'authenticated-specific-principal' AND requiredUserEID is null THEN
REJECT 'REQUIRED_USER_EID_MISSING'
IF authenticationRequirement != 'authenticated-specific-principal' AND requiredUserEID is not null THEN
REJECT 'REQUIRED_USER_EID_NOT_ALLOWED'
validateClientInstructions(clientInstructionsJSON)
Client Instruction Validation
IF clientInstructionsJSON.version is missing THEN
REJECT 'CLIENT_INSTRUCTIONS_VERSION_REQUIRED'
IF clientInstructionsJSON.options is empty THEN
REJECT 'CLIENT_INSTRUCTIONS_OPTIONS_REQUIRED'
FOR EACH option IN clientInstructionsJSON.options
REQUIRE option.key
REQUIRE option.label
REQUIRE option.description
REQUIRE option.sequence is non-empty
FOR EACH step IN option.sequence
actionDefinition := dispatcherRegistry.lookup(step.action)
IF actionDefinition not found THEN
REJECT 'UNKNOWN_CLIENT_ACTION'
validate(step.parameters, actionDefinition.parameterSchema)
Retrieval Validation
IF token does not match ^[a-f0-9]{64}$ THEN
REJECT 'InvalidRequest_ValueOfIncorrectFormat'
linkAction := lookupByToken(token)
IF linkAction not found THEN
REJECT 'NotFound_LinkAction'
IF linkAction.status = 'revoked' THEN
REJECT 'Gone_LinkAction_Revoked'
IF linkAction.status = 'expired' THEN
REJECT 'Gone_LinkAction_Expired'
IF linkAction.expiresAt <= now() THEN
REJECT 'Gone_LinkAction_Expired'
IF linkAction.remainingUseCount <= 0 THEN
REJECT 'Gone_LinkAction_Exhausted'
IF authenticationRequirement = 'authenticated-any-principal' AND caller not authenticated THEN
REJECT 'Unauthorized_AuthenticationRequired'
IF authenticationRequirement = 'authenticated-specific-principal' THEN
IF caller not authenticated THEN
REJECT 'Unauthorized_AuthenticationRequired'
IF caller.userEID != requiredUserEID THEN
REJECT 'Forbidden_IncorrectPrincipal'
atomically:
decrement remainingUseCount
insert LinkActionRetrievals row
3.13 Stored Procedures
The exact implementation is deferred, but the backend SHOULD expose procedures conceptually equivalent to:
api_linkActionCreate_v1(
p_authenticationRequirement,
p_requiredUserEID,
p_remainingUseCount,
p_expiresAt,
p_clientInstructionsJSON
) -> linkActionEID, rawToken, expiresAt
api_linkActionRetrieve_v1(
p_token,
p_retrievedByUserEID,
p_requestMetadataJSON
) -> remainingUseCountAfter, clientInstructionsJSON
api_linkActionRevoke_v1(
p_linkActionEID
) -> status
api_linkActionExpire_v1(
p_linkActionEID
) -> status
3.14 External Email Distribution via Notifications
LinkActions remain orthogonal to delivery, but invitation-style email delivery is handled through the existing notification system rather than through a separate InvitationEmails subsystem.
Architectural Rule
LinkActionsown finite-use token retrieval and client instruction delivery- the notification system owns email addressing, template rendering, request persistence, rendered snapshots, and provider delivery tracking
- invitation-style email delivery composes these two capabilities rather than introducing a third subsystem
In practical terms:
- a workflow may create a
LinkAction - the workflow may build a frontend URL carrying the opaque link-action token
- the workflow may pass that URL into notification render data
- the notification system renders and sends the email
- the recipient later retrieves the
LinkActionthrough the normal link-action retrieval API
Notification-System Integration
For invitation-style delivery, the notification system must support recipients who do not yet have a registered platform account.
The recipient model for this version is:
isUserRegistered = truerecipientUserIDis requiredrecipientEmailAddressstores the delivery snapshot
isUserRegistered = falserecipientUserIDmust be nullrecipientEmailAddressis required
This version is email-only. SMS and mixed multi-channel recipient models are out of scope.
Exactly one recipient coordinate is supported per notification request in this version.
Notification Persistence Model
This RFC relies on the existing notification persistence model, extended for external email recipients:
NotificationRequestsRenderedNotificationsNotificationDeliveryAttempts
No separate InvitationEmails, InvitationEmailLinks, or InvitationEmailDeliveryAttempts tables are defined.
Invitation Email Composition
Invitation-style emails are rendered from:
- an existing notification purpose and template
- structured notification render data
- a frontend URL containing the opaque
LinkActiontoken when link retrieval is needed
The notification system owns:
- recipient addressing
- template selection
- subject and body rendering
- durable request persistence
- rendered snapshot persistence
- delivery attempt persistence
The notification system does not own:
- token lifecycle
- token authentication requirements
- token use-count semantics
- client instruction execution
Team Invite Example
For team candidate invites:
- the backend creates a
LinkAction - the backend builds a frontend URL such as
/narp/app?linkAction={token} - the backend prepares a notification using a purpose such as
team.platform-invite - the notification render data includes inviter identity, invitation message, team context, and link-action URL
- the notification system records
NotificationRequests,RenderedNotifications, andNotificationDeliveryAttempts - the recipient receives the email and later retrieves the
LinkAction
4. Open Questions
At this stage there are no major architectural open questions recorded in this RFC. Remaining work is primarily implementation detail and API/error-shape refinement.
5. Security Considerations
- Opaque High-Entropy Tokens: Tokens must be difficult to guess and suitable for bearer-token use.
- Direct Token Storage: This RFC stores the opaque token value directly rather than storing a derived hash.
- Mutating Retrieval Endpoint: Retrieval must use
POST, notGET, because it consumes one use. - Atomic Decrement: Successful retrieval insertion and remaining-use decrement must occur in one transaction.
- No Arbitrary JavaScript:
clientInstructionsJSONmust be schema-validated and limited to a bounded dispatcher action registry. - Whole-Link Auth Enforcement: The backend enforces authentication before returning any client instructions.
- Finite Use: Every successful retrieval consumes one use; retries and prefetch risks must be handled at the frontend integration layer.
- Revocation and Expiry: Revoked, expired, and exhausted links must return failure without consumption.
- Recorded Email Delivery: Invitation-style email delivery must flow through the notification system so request, render, and delivery attempt records are preserved even for external recipients.
- External Recipient Constraints: External-recipient notifications must enforce the exact recipient-shape rules for this version so that address-backed delivery cannot silently masquerade as a registered-user notification.
6. Backward Compatibility
This RFC introduces a new orthogonal subsystem and extends the existing notification system for external email recipients without requiring all existing flows to migrate immediately.
Compatibility notes:
- existing linkless notification flows may remain unchanged
- invitation-style email flows should use the notification system rather than defining a separate invitation-email persistence model
- future invitation email flows may embed link-action frontend URLs without changing link-action core semantics
- existing frontend applications will require a dispatcher contract if they are to consume
clientInstructionsJSON
7. References
8. Author
ĀYŌDÈ Development Team
Codermerlin Academy Architecture