SuperAPI AI Directory

salla-app-lifecycle

General & other

View repository

Updated 10-16 01:12

About this skill

Salla App Lifecycle Flow

Keep your backend in sync with each merchant's relationship to your app — from install through trial, subscription, and uninstall. Follow the steps in order; complete each gate before moving on. Step 1 sets the webhook receiver with the Salla Partners MCP; Steps 2–6 are the runtime handlers you write.

Tools & MCPs

The App Events doc (https://docs.salla.dev/421413m0.md) is the authoritative source for every lifecycle payload shape. The Salla Partners MCP performs actions:

Tool Action What it does
salla_apps connect / get Set the webhook_url (lifecycle app events auto-deliver to it) / inspect app state

Lifecycle events are app.* events — auto-delivered, no subscribe call. The app is subscribed to its own app events by default, so app.installed, app.store.authorize, app.updated, app.uninstalled, app.trial.*, app.subscription.*, and app.settings.updated all arrive at your webhook_url automatically the moment they fire. You do not call salla_events action=subscribe for any of them — that action is only for non-app (store) events (order.*, product.*, …) → salla-webhooks.

All events arrive on your single webhook endpoint inside the standard envelope (event, merchant, created_at, data). Verify the signature, acknowledge fast with 200, then process asynchronously. Always upsert keyed by merchant. Prerequisites: signature + idempotency + fast 200 → salla-webhooks; token persistence from app.store.authorize → salla-app-auth.

Event Catalog (reference)

Event When What to do
app.installed First install Provision merchant resources, set defaults
app.store.authorize Install or token refresh Save/update access_token + refresh_token + expiry per merchant
app.updated Merchant updates the app Salla fires app.store.authorize right after — wait for it for new tokens
app.settings.updated Merchant changes app settings Apply data.settings — schema/validation owned by salla-app-settings
app.trial.started Trial begins Enable trial features
app.trial.expired Trial ended without upgrade Restrict access
app.trial.canceled Trial cancelled Restrict access
app.subscription.started Paid plan or addon activated Unlock features; branch on data.item_type
app.subscription.renewed Plan/addon renewed Confirm active; store new data.end_date / renew_date
app.subscription.expired Plan/addon lapsed Restrict access, notify merchant
app.subscription.canceled Plan/addon cancelled Restrict access
app.feedback.created Merchant leaves a review Log rating/comment

app.subscription.* fires for both plans and addons — data.item_type ("plan" | "addon") tells them apart. app.trial.* carries a smaller payload than subscription (no subscription_id, item_type, pricing, etc.) — see the reference. Full subscription payload + plan-state handling → salla-app-billing. Full per-event JSON (authoritative shapes from https://docs.salla.dev/421413m0.md) → references/lifecycle-payloads.md.


Step 0 — Discover

Ask before starting:

  1. Which lifecycle states does your app care about? (install only, or trial + subscription too?)
  2. What do you provision on install, and what must be cleaned up on uninstall (retention / GDPR policy)?
  3. Do you gate features on plan/trial state? (if so, pair with salla-app-billing)

Step 1 — Set the Webhook URL (lifecycle events auto-deliver)

Lifecycle events are app events — the app is subscribed to them by default, so they arrive the moment a webhook_url is set. There is no subscribe call; the one action is to configure the receiver with the Partners MCP:

  1. Configure the receiver: salla_apps action=connect, app_id, webhook_url, webhook_security_strategy: "signature". connect doesn't mint the signing secret — create/rotate it in the Partner Portal (https://portal.salla.partners/apps/{app_id}) and read the current value via salla_apps action=get (the webhook_secret field) before deploy. Store it in a secret manager/KMS (never in source or logs) — secure storage and HMAC verification are owned by salla-webhooks.
  2. That's it for delivery: app.installed, app.store.authorize, app.updated, app.uninstalled, app.trial.*, app.subscription.*, and app.settings.updated now flow to that URL automatically. (salla_events action=subscribe is for store events — order.*, product.*, … — which lifecycle handling doesn't need → salla-webhooks.)

Inspect the app's current configuration any time with salla_apps action=get, app_id.

Gate: "webhook_url set via salla_apps action=connect (app events auto-deliver to it — no subscribe call), and your endpoint returns 200?"


Step 2 — Handle the Install Flow

On first install Salla fires app.installed and app.store.authorize. Treat each handler as independent and idempotent; arrival order is not guaranteed.

Upsert the per-merchant record (create-or-update) in every handler. Any lifecycle event — e.g. app.trial.started — can land before app.store.authorize, so the row may not exist yet. Each handler creates the merchant row if missing and updates it if present, keyed by merchant. This keeps every handler safe in any arrival order and on re-install.

if (payload.event === "app.installed") {
  // Provision: create the merchant row, seed defaults, queue a welcome step.
  await db.merchants.upsert({
    where: { id: payload.merchant },
    create: {
      id: payload.merchant,
      status: "installed",
      installedAt: new Date(),
    },
    update: { status: "installed" },
  });
}

if (payload.event === "app.store.authorize") {
  // Persist tokens — see salla-app-auth for the full upsert + refresh rules.
  // Encrypt access_token/refresh_token at rest and keep them out of logs.
  const { access_token, refresh_token, expires, scope } = payload.data;
  await db.merchants.upsert({
    where: { id: payload.merchant },
    create: {
      id: payload.merchant,
      accessToken: access_token,
      refreshToken: refresh_token,
      tokenExpiresAt: new Date(expires * 1000),
      scope,
    },
    update: {
      accessToken: access_token,
      refreshToken: refresh_token,
      tokenExpiresAt: new Date(expires * 1000),
      scope,
    },
  });
}

Manage the merchant/token tables with a versioned migration tool (versioned ALTER TABLEs). This keeps the schema in lockstep with the code as you add or change columns, where a create-on-boot step would skip an already-existing table.

Gate: "Install of a demo store creates the merchant row and persists tokens, in any arrival order?"


Step 3 — Handle the Update Flow

When the merchant updates the app, Salla fires app.updated then immediately app.store.authorize with fresh tokens.

  • Treat app.updated as a signal only — do not expect tokens in its payload.
  • Let your existing app.store.authorize handler persist the new tokens.

Gate: "An app update refreshes stored tokens via the authorize handler, not the update handler?"


Step 4 — Handle Uninstall Cleanup

app.uninstalled means the merchant removed your app. Cleanup is an App Store requirement — handle it per your retention/GDPR policy.

if (payload.event === "app.uninstalled") {
  await jobs.enqueue("cleanupMerchant", { merchantId: payload.merchant });
  // In the job: revoke cached tokens, delete or anonymize PII, cancel scheduled work,
  // mark merchant status = "uninstalled". Keep only what your policy/law requires.
}

Document what you store and how it's deleted on uninstall — it's reviewed at submission.

Gate: "Uninstall enqueues cleanup and the job revokes tokens + removes/anonymizes PII?"


Step 5 — Handle Trial & Subscription Events

Event Effect
app.trial.started Enable trial-tier features; record trial end
app.trial.expired Downgrade/restrict until they subscribe
app.trial.canceled Restrict immediately

A merchant typically flows trial.started → (subscription.started if they convert | trial.expired if they don't). Minimum subscription handling here — full logic in salla-app-billing:

if (payload.event === "app.subscription.started") {
  const { item_type, item_slug, end_date } = payload.data; // item_type: "plan" | "addon"
  if (item_type === "addon") {
    await entitlements.activateAddon(payload.merchant, item_slug, end_date);
  } else {
    await merchants.activatePlan(payload.merchant, payload.data);
  }
}

Watch data.store_type (development | demo | live) to avoid treating test-store subscriptions as real revenue.

Gate: "Trial and subscription events transition the merchant's stored status, branching on item_type?"


Step 6 — Harden: Reliability & State Machine

Non-negotiable for every handler above:

  • Verify the signature first (HMAC-SHA256, timing-safe) — reject otherwise.
  • Acknowledge fast with 200, then process async — Salla retries failed deliveries; the timeout + retry policy is owned by salla-webhooks.
  • Be idempotent — key on a stable discriminator from the payload (e.g. subscription_id) or a hash of the raw body. (created_at is second-resolution, so ${merchant}:${event}:${created_at} alone can collide.)
  • Upsert so the same merchant can re-install and re-authorize.
  • Handle async-job failure — the 200 is already sent, so a throw in the queued worker is invisible to Salla. Route failed jobs to a dead-letter queue, retry with backoff (idempotent), and alert on exhausted retries so a dropped install/uninstall doesn't go unnoticed.

Recommended merchant state machine — persist status per merchant and transition from the handlers above:

        app.installed
              │
              ▼
  ┌──────► installed ──────┐
  │           │            │ app.trial.started
  │           │            ▼
  │           │          trial ──── app.trial.expired/canceled ──► restricted
  │           │            │ app.subscription.started
  │           ▼            ▼
  │        active ◄── app.subscription.renewed
  │           │
  │           │ app.subscription.expired/canceled
  │           ▼
  │       restricted
  │           │ app.subscription.started (merchant re-purchases)
  └───────────┘
              │ app.uninstalled (from any state)
              ▼
        uninstalled  (run cleanup)

Gate features on status (and on addon entitlements — see salla-app-billing).

Gate: "Every handler verifies signature, returns 200 fast, is idempotent, and updates status along the state machine?"


Red Flags

Assumptions about lifecycle delivery that hold in a quick test and fail with a real merchant. If one of these is your plan, re-read the named step.

Tempting thought Why it's wrong
"app.installed arrives before app.store.authorize." Order isn't guaranteed — any event can land first. Make each handler independent and idempotent, upserting by merchant (Step 2).
"app.updated carries the new tokens." It's a signal only — tokens come in the app.store.authorize that follows. Don't read tokens from the update payload (Step 3).
"insert is fine — installs only happen once." Merchants re-install and re-authorize. Insert-only throws or duplicates; always upsert keyed by merchant (Steps 2, 6).
"The async job is queued, so the work is safe." The 200 is already sent — a throw in the worker is invisible to Salla. Dead-letter, retry idempotently, and alert (Step 6).
"${merchant}:${event}:${created_at} is a unique key." created_at is second-resolution; two same-type events in one second collide. Add subscription_id or a raw-body hash (Step 6).
"I'll bill test-store subscription events as revenue." Watch data.store_type — only live is real; development/demo installs would corrupt revenue and entitlements (Step 5).
"Uninstall just flips a status flag." Cleanup (revoke tokens, delete/anonymize PII) is an App Store requirement reviewed at submission — enqueue the full job (Step 4).

Key Resources

Resource URL
App Events docs https://docs.salla.dev/421413m0.md
Webhooks (security) salla-webhooks skill
Token handling salla-app-auth skill
Plan/trial state salla-app-billing skill
Partners Portal https://salla.partners
Telegram community https://t.me/salladev