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

  1. Introduction
  2. Motivation
  3. Specification
  4. Open Questions
  5. Security Considerations
  6. Backward Compatibility
  7. References
  8. 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

Missing Capabilities

  1. Opaque tokens that retrieve server-stored client instructions
  2. Authentication requirements attached to a link as a whole
  3. Finite-use link semantics with a remaining-use countdown
  4. Explicit lifecycle control for revocation and expiry
  5. Successful consumptive retrieval audit history
  6. Multi-option instruction payloads that the frontend can process locally

Design Goals

  1. Make link actions independent from any delivery mechanism
  2. Keep all authoritative semantics on the server
  3. Support unauthenticated, authenticated-any-principal, and authenticated-specific-principal retrieval
  4. Support finite-use links with atomic usage decrement
  5. Return declarative client instructions rather than router-specific URLs or arbitrary JavaScript
  6. Keep the initial design simpler than a generalized backend action execution framework
  7. 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:

  1. Orthogonal to Delivery: A LinkAction does not require email and does not assume email.
  2. Opaque Token: The public token is an unguessable bearer token stored server-side as an opaque value.
  3. Server-Stored Semantics: Authoritative lifecycle, authentication requirements, and client instructions live in the database.
  4. Consumptive Retrieval: Retrieval decrements remaining uses and is therefore a mutating operation.
  5. Declarative Client Instructions: The backend returns structured client instructions for a bounded frontend dispatcher.
  6. Finite Use Only: Every LinkAction has a finite remaining-use count.

3.2 Core Concepts

LinkAction

A LinkAction is one tokenized retrieval unit.

Each LinkAction has:

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:

  1. validates the token and lifecycle
  2. validates the authentication requirement
  3. records a successful retrieval
  4. decrements remainingUseCount
  5. 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:

For version 1, the token format is a 64-character lowercase hexadecimal string:

^[a-f0-9]{64}$

The token MUST NOT contain:

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:

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

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

  1. Retrieval is the consumptive act.
  2. The backend does not observe which client instruction option is chosen later.
  3. Retrieval failures do not decrement usage and do not create retrieval rows.
  4. 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

Authentication reminder:

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:

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:

  1. read data.clientInstructions
  2. verify version is supported
  3. verify the instruction set shape is supported
  4. present available options to the user
  5. dispatch the selected option’s sequence through 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:

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

In practical terms:

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:

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:

No separate InvitationEmails, InvitationEmailLinks, or InvitationEmailDeliveryAttempts tables are defined.

Invitation Email Composition

Invitation-style emails are rendered from:

The notification system owns:

The notification system does not own:

Team Invite Example

For team candidate invites:

  1. the backend creates a LinkAction
  2. the backend builds a frontend URL such as /narp/app?linkAction={token}
  3. the backend prepares a notification using a purpose such as team.platform-invite
  4. the notification render data includes inviter identity, invitation message, team context, and link-action URL
  5. the notification system records NotificationRequests, RenderedNotifications, and NotificationDeliveryAttempts
  6. 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

  1. Opaque High-Entropy Tokens: Tokens must be difficult to guess and suitable for bearer-token use.
  2. Direct Token Storage: This RFC stores the opaque token value directly rather than storing a derived hash.
  3. Mutating Retrieval Endpoint: Retrieval must use POST, not GET, because it consumes one use.
  4. Atomic Decrement: Successful retrieval insertion and remaining-use decrement must occur in one transaction.
  5. No Arbitrary JavaScript: clientInstructionsJSON must be schema-validated and limited to a bounded dispatcher action registry.
  6. Whole-Link Auth Enforcement: The backend enforces authentication before returning any client instructions.
  7. Finite Use: Every successful retrieval consumes one use; retries and prefetch risks must be handled at the frontend integration layer.
  8. Revocation and Expiry: Revoked, expired, and exhausted links must return failure without consumption.
  9. 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.
  10. 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:


7. References


8. Author

ĀYŌDÈ Development Team
Codermerlin Academy Architecture