Paymug is a self-hosted Lemon Squeezy / Polar alternative for selling digital products and subscriptions. Connect PayPal or Stripe, get paid directly, and manage your store from one dashboard.
Built with Next.js 16, Tailwind CSS, Cloudflare D1, and Drizzle ORM.
| Demos | URL | Login |
|---|---|---|
| Storefront | demo.paymug.co | — |
| Admin | demo.paymug.co/dashboard | demo@paymug.co / demo1234 |
| Customer | demo.paymug.co/customer | demo@paymug.co / demo1234 |
- Sell your way — storefront, hosted checkout, one-time products, and subscriptions
- Get paid directly — connect PayPal or Stripe with sandbox and live modes
- Deliver automatically — files, license keys, and private GitHub access
- Grow sales — discounts, email campaigns, and affiliates
- Run everything in one place — customers, orders, refunds, analytics, and a buyer portal
- Own your stack — self-host on Cloudflare and extend with API keys
npm install
cp .env.example .env.local # optional — defaults work for local dev
# Apply D1 schema to the local Miniflare SQLite file
npm run db:migrate:local
npm run devOpen http://localhost:3000.
| Script | Purpose |
|---|---|
npm run db:generate |
Generate SQL migrations from src/db/schema.ts |
npm run db:migrate:local |
Apply migrations to local D1 |
npm run db:migrate:remote |
Apply migrations to remote D1 (needs real database_id) |
Schema: src/db/schema.ts · Client: src/db/index.ts · Queries: src/lib/db.ts
Payment credentials are read from environment variables and are never saved to the database. The dashboard is used only to choose which provider customers see at checkout.
- Create a PayPal app at developer.paypal.com (or a Stripe account at dashboard.stripe.com).
- Set the provider credentials as environment variables on the deployment (see the Environment table below for the exact names; one set for sandbox and one for live).
- In Paymug: Dashboard → Payments → choose the active provider and click
Set up webhook automatically for sandbox/live (creates the PayPal
webhook pointing at the app; requires a public HTTPS app URL). For Stripe,
create the webhook endpoint in the Stripe dashboard and set
STRIPE_*_WEBHOOK_SECRET. - Publish a product and open its checkout link.
Use sandbox buyer accounts or Stripe test keys to test purchases.
- Sign up → store created
- Connect PayPal
- Create & publish products
- Share
/buy/{id}or the storefront homepage/ - Buyer pays your PayPal → order marked paid → delivery content shown
| Method | Path | Purpose |
|---|---|---|
| POST | /api/auth/signup |
Create merchant |
| POST | /api/auth/login |
Session login |
| GET/POST | /api/products |
List / create products |
| GET | /api/payments/paypal/connect |
PayPal credential status (from env) |
| POST | /api/payments/paypal/create-order |
Create PayPal order (buyer) |
| POST | /api/payments/paypal/capture-order |
Capture payment |
| Variable | Description |
|---|---|
AUTH_SECRET |
JWT session signing key |
ENCRYPTION_SECRET |
Encrypts integration secrets at rest |
NEXT_PUBLIC_APP_URL |
Absolute origin for PayPal/Stripe return and webhook URLs |
PAYMUG_LICENSE_API_URL |
Paymug license activation and validation origin (defaults to https://api.paymug.co) |
PAYPAL_SANDBOX_CLIENT_ID |
PayPal sandbox app Client ID |
PAYPAL_SANDBOX_CLIENT_SECRET |
PayPal sandbox app Client Secret |
PAYPAL_LIVE_CLIENT_ID |
PayPal live app Client ID |
PAYPAL_LIVE_CLIENT_SECRET |
PayPal live app Client Secret |
STRIPE_SANDBOX_SECRET_KEY |
Stripe test secret key (sk_test_…) |
STRIPE_SANDBOX_WEBHOOK_SECRET |
Stripe test webhook signing secret (whsec_…) |
STRIPE_LIVE_SECRET_KEY |
Stripe live secret key (sk_live_…) |
STRIPE_LIVE_WEBHOOK_SECRET |
Stripe live webhook signing secret (whsec_…) |
EMAIL_FROM |
Verified Paymug sender on your Cloudflare Email Service domain |
EMAIL_REPLY_TO |
Optional customer reply address |
GITHUB_CLIENT_ID |
GitHub OAuth App client ID |
GITHUB_CLIENT_SECRET |
GitHub OAuth App client secret |
Set the GitHub OAuth App callback URL to
<NEXT_PUBLIC_APP_URL>/api/github/oauth/callback.
AUTH_SECRET, ENCRYPTION_SECRET, and NEXT_PUBLIC_APP_URL are required at
runtime: the app fails closed (session signing, secret decryption, and URL
construction refuse to run) instead of falling back to a default or to the
request Host header.
- In Cloudflare, open Compute → Email Service → Email Sending and onboard the sending domain.
- Use a Workers Paid plan when sending to arbitrary customer addresses.
- Configure
EMAIL_FROMwith an address on the onboarded domain, plus optionalEMAIL_REPLY_TO. Store addresses are used only for replies; merchant alerts go to the Paymug account email. - The
EMAILWorker binding is already declared inwrangler.jsonc.
Deploy Paymug to your Cloudflare account in one click:
The Cloudflare resources are intentionally not tied to the demo account. On the first deployment, Wrangler provisions a D1 database and R2 bucket in the deploying user's Cloudflare account. Each fork therefore has its own data and product storage.
npm run deployThe deploy command applies pending D1 migrations before building and deploying the Worker.
Cloudflare's Deploy to Cloudflare button clones its source into a new repository
in the deploying user's GitHub account and connects that repository to Workers
Builds. The button source must be public, but the newly created UserGitRepo
can be private. If the deployment must start directly from the private
Paymug/paymug repository, use Cloudflare's authenticated repository import
instead of a public Deploy button.
The update source is hardcoded as Paymug/paymug; users do not configure the
source or destination repository name. GITHUB_REPOSITORY identifies
UserGitRepo automatically inside GitHub Actions.
- Give the purchaser's GitHub account read access to the private
Paymug/paymugrepository. - In
UserGitRepo, open Settings → Secrets and variables → Actions and add one secret:
| Secret | Value |
|---|---|
UPSTREAM_TOKEN |
A GitHub credential belonging to that purchaser with Contents read access to Paymug/paymug |
- Ensure GitHub Actions is enabled for
UserGitRepo. The included workflow declarescontents: write, so its repository-scopedGITHUB_TOKENcan push the merged update back toUserGitRepowithout another token.
After setup:
.github/workflows/sync-upstream.ymlchecksPaymug/paymug/latestevery six hours and can also be run manually. It safely merges new upstream commits and pushesUserGitRepo/main.- That push triggers the Cloudflare Workers Build already connected during the one-click deployment. The sync workflow does not deploy a second time.
/api/deploymentsexposes the current official Paymug version. Settings → About → Check for update reads that endpoint, so no GitHub token is stored in the Worker runtime.- Build metadata discovers
UserGitRepofrom the Git remote; no repository environment variable is required. - A merge conflict stops the update instead of overwriting user changes.
Resolve it in
UserGitRepo, then rerun Sync upstream updates. .github/workflows/deploy.ymlremains an optional manual fallback. It needsCLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKEN, but normal Workers Builds deployments do not.
Keep runtime application secrets such as AUTH_SECRET, ENCRYPTION_SECRET, and
payment credentials in Cloudflare. Automated deployments preserve variables and
secrets already configured on the Worker.
- Stripe and Paddle as additional gateways
- Custom domains
