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 --uiConfig-first setup
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
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
| Area | Details |
|---|---|
| Products | Public product metadata for pricing and account screens. |
| Checkout | Polar checkout sessions for one-time and subscription products. |
| Portal | Customer sessions for billing management. |
| Billing status | Current customer, plan, features, limits, and active product state. |
| Usage | Meter and entitlement helpers for usage-aware products. |
| Webhooks | Verified 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, andAPP_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-usagesends the caller'sidempotencyKeyto Polar as the eventexternalId<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 noOrigin(your own server, a script) are unaffected. If your frontend runs on another origin, list it inallowedOrigins, using the same syntax asserverActions.allowedOrigins. - Test checkout return URLs, portal return URLs, webhook signatures, and entitlement checks.
Use Polar for products, checkout, customer portals, subscriptions, webhooks, and entitlement-aware app flows.