Farm.js

Polar Integration

Polar is a good fit for SaaS products, open-source sponsorships, digital products, and entitlement-aware app features.

Install from the CLI

farm add integration polar --ui

Config-first setup

src/lib/integrations.ts
import { polar } from "@farm.js/polar";

export const integrations = {
  billing: polar({
    accessToken: process.env.POLAR_ACCESS_TOKEN,
    billing: {
      resolveOwner(ctx) {
        const userId = ctx.req.get<string>("user.id");
        return userId ? { id: userId, kind: "user" } : null;
      },
    },
  }),
};

POLAR_WEBHOOK_SECRET is read from the environment when webhook routes are configured.

With no instance, Farm constructs the Polar SDK from accessToken and server. The token can be passed directly or read from POLAR_ACCESS_TOKEN.

Choose SDK ownership

Let Farm construct Polar

Use the config-first example above for the common path. Farm owns SDK construction, while the application still supplies billing ownership, products, meters, routes, and webhook behavior.

Provide an application-owned instance

import { Polar } from "@polar-sh/sdk";
import { polar } from "@farm.js/polar";

const polarClient = new Polar({
  accessToken: process.env.POLAR_ACCESS_TOKEN!,
  server: "sandbox",
});

export const billing = polar({
  instance: polarClient,
  server: "sandbox",
  billing: {
    resolveOwner: () => null,
  },
});

Farm uses the supplied SDK directly, so the integration does not require accessToken. server still describes the integration environment and defaults from POLAR_SERVER. When both an instance and token are supplied, the instance wins.

Usage

Checkout
const checkout = await api.billing.checkout.post({
  body: {
    productId: "pro",
    customerEmail: user.email,
    successPath: "/dashboard",
  },
});

Billing state lives in Polar: the routes read it through the Polar API, and Farm stores no billing records of its own. Webhooks verify each event and hand it to your onEvent handler, which is the place to update any records your app keeps.

What Polar adds

AreaDetails
ProductsPublic product metadata for pricing and account screens.
CheckoutPolar checkout sessions for one-time and subscription products.
PortalCustomer sessions for billing management.
Billing statusCurrent customer, plan, features, limits, and active product state.
UsageMeter and entitlement helpers for usage-aware products.
WebhooksVerified subscription and order events for your onEvent handlers.

Common callers

const products = await api.billing.products.get();
const status = await api.billing.status.get();

const checkout = await api.billing.checkout.post({
  body: {
    productId: "pro",
    successPath: "/dashboard",
    cancelPath: "/pricing",
  },
});

Portal flow

const portal = await api.billing.portal.post({
  body: {
    returnTo: "/settings/billing",
  },
});

if (portal.data?.redirectTo) {
  window.location.href = portal.data.redirectTo;
}

Checkout and portal return paths must be same-origin, root-relative application paths. Protocol-relative paths such as //example.com are rejected.

When billing lives in Convex

If your app runs Polar as the @convex-dev/polar Convex component, keep billing there and do not also register polar() from @farm.js/polar. The Convex integration stops farm dev and farm build when both are set up, unless you set home to say which side owns billing. To receive Polar's webhooks on your own domain, use component("@convex-dev/polar", { webhooks: true }) from @farm.js/convex in convex({ use }); see Billing with Polar running in Convex.

When Polar is a good fit

Polar works especially well when the product is developer-facing, open-source, sponsor-backed, or selling digital access. The Farm integration keeps the same caller shape as other billing providers, so moving between Stripe, Autumn, and Polar does not force your app UI to learn a new local API style.

Production notes

  • Set POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_SERVER, and APP_BASE_URL.
  • Use sandbox/server config for local testing and production config for live billing.
  • Store the external customer ID with the billing owner so portal and status reads stay stable.
  • report-usage sends the caller's idempotencyKey to Polar as the event externalId <external customer id>:<key>, so one customer's key can never claim another's event.
  • The billing POST routes act for the signed-in caller from its session cookie, so a request a browser sends from another origin is refused with 403: a page on another site cannot report usage, start a checkout or open the portal for your visitor. Calls with no Origin (your own server, a script) are unaffected. If your frontend runs on another origin, list it in allowedOrigins, using the same syntax as serverActions.allowedOrigins.
  • Test checkout return URLs, portal return URLs, webhook signatures, and entitlement checks.