Creem Setup

Configure Creem products, Edge secrets, and the webhook.

Creem runs outside the desktop app. The Electron template only ships public product IDs in the renderer and server-side Edge Functions that hold the Creem API key, verify webhooks, and write Supabase entitlement rows.

Create products

Create the products you plan to sell in Creem:

Plan in templateCreem product typeApp env
Pro monthlySubscriptionVITE_CREEM_PRODUCT_PRO_MONTHLY=prod_...
Pro yearlySubscriptionVITE_CREEM_PRODUCT_PRO_YEARLY=prod_...
LifetimeOne-timeVITE_CREEM_PRODUCT_LIFETIME=prod_...

Put those IDs in soar-electron/.env. They are renderer-public and safe to ship. Do not put CREEM_API_KEY in the app .env.

Creem API selection is controlled by the Edge secret CREEM_SERVER_IDX:

ValueAPI base
0Production API (https://api.creem.io)
1Test API (https://test-api.creem.io)

Use test mode while validating checkout and webhook handling.

Set Edge secrets

Set secrets on the Supabase project:

supabase secrets set \
  CREEM_API_KEY=creem_test_xxx \
  CREEM_WEBHOOK_SECRET=whsec_xxx \
  CREEM_SERVER_IDX=1 \
  --project-ref <ref>

Or copy supabase/functions/.env.example to the gitignored supabase/functions/.env, fill it in, and upload it:

supabase secrets set --env-file supabase/functions/.env --project-ref <ref>
SecretUsed byNotes
CREEM_API_KEYcreate-checkout, customer-portal, activate-licenseServer-side Creem API key.
CREEM_WEBHOOK_SECRETcreem-webhookMust match the signing secret configured in Creem.
CREEM_SERVER_IDXAll Creem functions0 production, 1 test.
BILLING_SUCCESS_URLcreate-checkoutOptional hosted page that bounces the browser back to the desktop app.

Supabase injects SUPABASE_URL, SUPABASE_ANON_KEY, and SUPABASE_SERVICE_ROLE_KEY into Edge Functions. Do not set those in the app or in the Edge secret file.

Deploy functions

Apply the billing migration before deploying functions:

supabase db push --project-ref <ref>

Then deploy:

pnpm supabase:deploy
pnpm supabase:deploy <project-ref>

The helper script deploys every folder under supabase/functions/ except _shared. It also passes --no-verify-jwt for creem-webhook; all other functions keep JWT verification on through supabase/config.toml.

Expected functions:

FunctionJWTRole
create-checkoutOnCreates a Creem checkout for the signed-in user and records a pending payment row.
customer-portalOnCreates a Creem portal URL for the signed-in user's stored customer id.
activate-licenseOnOptional license-key activation and deactivation.
creem-webhookOffPublic Creem webhook; verifies creem-signature before writing entitlements.

Verify deploys with:

supabase functions list --project-ref <ref>

Configure the webhook

In Creem, add this webhook URL:

https://<ref>.supabase.co/functions/v1/creem-webhook

Set the webhook signing secret to the same value you stored as CREEM_WEBHOOK_SECRET.

The current handler processes this implemented event set:

EventBehavior
checkout.completedUpdates the payments row matching object.request_id / order_no to status = 'success', stores Creem order/customer ids, and returns.
subscription.paidUpserts the signed-in user's subscriptions row from object.metadata.userId, product, customer, and subscription period data.

Other event types are ignored today. If you need cancellation, refund, pause, or past-due handling, extend supabase/functions/_shared/creem.ts and keep the client read model unchanged.

Webhook trust and idempotency:

  • creem-webhook reads the raw request body before JSON parsing.
  • verifyCreemSignature() recomputes the HMAC-SHA256 using CREEM_WEBHOOK_SECRET and compares it to the creem-signature header.
  • payments.order_no is unique, so repeated checkout.completed deliveries converge on the same row.
  • subscriptions.user_id is unique, so repeated subscription.paid deliveries upsert the same entitlement row.
  • Handler errors return 500 so Creem can retry.

Checkout return page

A desktop app cannot rely on the browser redirect directly landing in the app, so BILLING_SUCCESS_URL can point at a tiny hosted bounce page. The template includes supabase/pay-success.sample.html.

Before hosting it, update the sample's stale soar:// link to the current scheme:

soar-electron://billing?status=success&orderId=...

Use your rebranded scheme after running pnpm rebrand --scheme.

Then set the hosted page as an Edge secret:

supabase secrets set BILLING_SUCCESS_URL=https://your-domain.com/pay-success --project-ref <ref>

create-checkout appends status=success&orderId=<orderNo> before sending the URL to Creem. If BILLING_SUCCESS_URL is unset, Creem falls back to the product's configured redirect.

Local testing

Serve functions locally with a local Edge env file:

supabase functions serve --env-file supabase/functions/.env

Local functions run under:

http://localhost:54321/functions/v1/<name>

Use supabase start too if you want the local Postgres/Auth stack. For a full hosted sandbox checkout, remote deploys are usually simpler because Creem needs to reach the webhook URL.

After a sandbox checkout, inspect rows:

select order_no, user_id, product_id, status, customer_id, updated_at
from public.payments
order by created_at desc
limit 10;

select user_id, product_id, product_type, status, period_end, customer_id, subscription_id
from public.subscriptions
order by updated_at desc
limit 10;

Production checklist

  • VITE_BILLING_ENABLED=true in the desktop build.
  • VITE_CREEM_PRODUCT_* values match the Creem environment you are using.
  • Billing migration is applied.
  • CREEM_API_KEY, CREEM_WEBHOOK_SECRET, and CREEM_SERVER_IDX are set as Supabase Edge secrets.
  • pnpm supabase:deploy has deployed all four functions.
  • Creem webhook points at https://<ref>.supabase.co/functions/v1/creem-webhook.
  • BILLING_SUCCESS_URL uses soar-electron://billing or your rebranded scheme if you host the bounce page.
  • A sandbox checkout writes both payments and subscriptions.
  • You have extended webhook handling if your product needs cancellations, refunds, or past-due status changes reflected in-app.

On this page