Async Resolution

Some workflows cannot finish during the original request. Payment confirmation, AI processing, email verification, and external API callbacks often complete later. FRL supports this with promise fields and resolved webhook handlers.

Create a Promise Field

Use promise(webhook, promiseId) in a process block.

post deposits {
  process {
    $post.payment = promise(
      "/webhook/stripe",
      $post.paymentReference
    );
  }

  schema {
    paymentReference: { type: "string", required: true, immutable: true }
    payment: { type: "object", maxDepth: 2 }
    paid: { type: "boolean" }
    amountPaid: { type: "number", min: 0 }
  }
}

The promise stores enough information for Frontbacked to match a later webhook to the original post.

Handle the Webhook

Use on resolved to update the post when a matching webhook arrives.

post deposits {
  on resolved payment once
    from "/webhook/stripe"
    match $resolved.body.reference_id
  {
    $post.paid = true;
    $post.amountPaid = $resolved.amount_total;
  }
}

The handler above says:

PartMeaning
paymentThe promise field on the post.
onceTreat resolution as idempotent where supported.
from "/webhook/stripe"Only handle webhook events from this path.
match $resolved.body.reference_idExtract the promise id from the webhook payload.
Handler bodyAssign final values into $post.

Resolved Context

During a resolved handler, $resolved contains the webhook request data. Common fields include body, headers, or payload values from the integration you are handling.

on resolved thumbnail
  from "/webhook/ai"
  match $resolved.header.["x-promise-id"]
{
  $post.imageUrl = $resolved.image.url;
}

Bracket access is useful for header names or keys that contain characters such as hyphens.

Validate After Resolution

After a resolved handler updates $post, the schema runs again. This means resolved data must still match your declared schema.

schema {
  imageUrl: { type: "string", max: 500 }
  paid: { type: "boolean" }
  amountPaid: { type: "number", min: 0 }
}

Keep schemas wide enough to allow pending and completed states, but narrow enough to reject malformed callback payloads.

Async Design Pattern

A common async post has:

  1. A client-submitted reference field marked immutable.
  2. A promise field created in process.
  3. A resolved handler that matches the callback.
  4. Final fields that are written only by the resolved handler.
  5. Permission rules that prevent frontend users from spoofing completion.

Example:

post verifications {
  process {
    $post.lookup = promise("/webhook/identity", $post.reference);
  }

  schema {
    id: { type: "string", required: true, immutable: true }
    userId: { type: "string", required: true, immutable: true }
    reference: { type: "string", required: true, immutable: true }
    lookup: { type: "object", maxDepth: 2 }
    status: {
      type: "string",
      enum: ["pending", "approved", "rejected"],
      required: true,
      immutableUnless: $access.edit
    }
    checkedAt: { type: "datetime" }
  }

  create require[
    $user.id == $post.userId
  ]

  edit require[
    $access.edit
  ]

  on resolved lookup once
    from "/webhook/identity"
    match $resolved.body.reference
  {
    $post.status = $resolved.body.status;
    $post.checkedAt = $resolved.body.checkedAt;
  }
}

The frontend can create a pending verification, but only the trusted webhook or an editor can change the protected status field.