← Provider replacement walkthrough

Commerce Protocol Example · composition/app-backend.mjs

Source snapshot · 2026-09-09

SHA-256 4b2b617634026ab2d1d936c29e246f47eb2f2d31769ff204df745785f9f6f55d
1import { Database } from "bun:sqlite";2import { createCommerceClient } from "./commerce-client.mjs";3import { createErasureLedger } from "../erasure.mjs";4 5// resolveSession is the host app's authentication boundary, supplied by the caller.6export function startAppBackend({7  path,8  providers,9  receiver,10  resolveSession,11  resolveStoreUser,12}) {13  const db = new Database(path, { create: true });14  db.exec(15    "CREATE TABLE IF NOT EXISTS erasure_requests (user_id TEXT, provider TEXT, PRIMARY KEY(user_id, provider))",16  );17  const erased = createErasureLedger(db);18  const clients = Object.fromEntries(19    Object.entries(providers).map(([name, config]) => [20      name,21      createCommerceClient(config),22    ]),23  );24  const inFlight = new Map();25  let selected = Object.keys(clients)[0];26  async function drainErasure() {27    for (const row of db.query("SELECT * FROM erasure_requests").all()) {28      try {29        receiver.eraseUser(row.user_id);30        await Promise.allSettled([...(inFlight.get(row.user_id) ?? [])]);31        const result = await clients[row.provider].call("eraseUser", {32          userId: row.user_id,33        });34        if (result.accepted && result.status === "completed")35          db.query(36            "DELETE FROM erasure_requests WHERE user_id = ? AND provider = ?",37          ).run(row.user_id, row.provider);38      } catch {39        /* The durable request is retried by the app's worker. */40      }41    }42    return db.query("SELECT count(*) AS count FROM erasure_requests").get()43      .count;44  }45  const server = Bun.serve({46    hostname: "127.0.0.1",47    port: 0,48    maxRequestBodySize: 32768,49    async fetch(request) {50      const userId = await resolveSession(request);51      if (!userId || erased.has(userId))52        return new Response("Unauthenticated", { status: 401 });53      const url = new URL(request.url);54      try {55        if (url.pathname === "/purchase" && request.method === "POST") {56          const input = await request.json();57          if (erased.has(userId))58            return new Response("Unauthenticated", { status: 401 });59          if (input.store === "amazon" || input.store === "horizon") {60            const storeUser = await resolveStoreUser?.(request, input.store);61            if (!storeUser || input[input.store]?.userId !== storeUser)62              return new Response(63                "Store account is not linked to this session",64                { status: 403 },65              );66          }67          if (erased.has(userId))68            return new Response("Unauthenticated", { status: 401 });69          const work = clients[selected].fulfill(input, {70            userId,71            productId: "premium.monthly",72          });73          const pending = inFlight.get(userId) ?? new Set();74          pending.add(work);75          inFlight.set(userId, pending);76          let result;77          try {78            result = await work;79          } finally {80            pending.delete(work);81            if (!pending.size) inFlight.delete(userId);82          }83          // A deletion can race the upstream calls; never return access afterwards.84          return Response.json(erased.has(userId) ? { access: false } : result);85        }86        if (url.pathname === "/access" && request.method === "GET") {87          const result = await clients[selected].call("entitlements", {88            userId,89          });90          return erased.has(userId)91            ? new Response("Unauthenticated", { status: 401 })92            : Response.json(result);93        }94        if (url.pathname === "/account" && request.method === "DELETE") {95          db.transaction(() => {96            erased.remember(userId);97            for (const name of Object.keys(clients))98              db.query(99                "INSERT OR IGNORE INTO erasure_requests VALUES (?,?)",100              ).run(userId, name);101          })();102          receiver.eraseUser(userId);103          const pending = await drainErasure();104          return Response.json({105            accepted: true,106            status: pending ? "queued" : "completed",107          });108        }109        return new Response("Not found", { status: 404 });110      } catch {111        return Response.json(112          { error: "Commerce provider unavailable; retry the request." },113          { status: 503 },114        );115      }116    },117  });118  return {119    url: `http://127.0.0.1:${server.port}`,120    select(name) {121      if (!clients[name]) throw new Error("Unknown provider");122      selected = name;123    },124    drainErasure,125    async close() {126      await server.stop(true);127      db.close();128    },129  };130}131