Sell digital products on your own terms
16
stars
38
commits
TypeScript
primary language
Sep 6, 2026
updated
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 |
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 dev
Open 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.
STRIPE_*_WEBHOOK_SECRET.Use sandbox buyer accounts or Stripe test keys to test purchases.
/buy/{id} or the storefront homepage /| 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.
EMAIL_FROM with an address on the onboarded domain, plus optional EMAIL_REPLY_TO. Store addresses are used only for replies; merchant alerts go to the Paymug account email.EMAIL Worker binding is already declared in wrangler.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 deploy
The 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.
Paymug/paymug repository.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 |
UserGitRepo. The included workflow
declares contents: write, so its repository-scoped GITHUB_TOKEN can push
the merged update back to UserGitRepo without another token.After setup:
.github/workflows/sync-upstream.yml checks Paymug/paymug/latest every six
hours and can also be run manually. It safely merges new upstream commits and
pushes UserGitRepo/main./api/deployments exposes the current official Paymug version.
Settings → About → Check for update reads that endpoint, so no GitHub
token is stored in the Worker runtime.UserGitRepo from the Git remote; no repository
environment variable is required.UserGitRepo, then rerun Sync upstream updates..github/workflows/deploy.yml remains an optional manual fallback. It needs
CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_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.
See docs/README.md for developer, API, and AI-agent guides.
The documentation plan and completeness tracker are in
docs/DOCS.md.
38 commits
TypeScript
97.5%
CSS
2.3%
Sell digital products on your own terms
16
stars
38
commits
TypeScript
primary language
Sep 6, 2026
updated
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 |
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 dev
Open 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.
STRIPE_*_WEBHOOK_SECRET.Use sandbox buyer accounts or Stripe test keys to test purchases.
/buy/{id} or the storefront homepage /| 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.
EMAIL_FROM with an address on the onboarded domain, plus optional EMAIL_REPLY_TO. Store addresses are used only for replies; merchant alerts go to the Paymug account email.EMAIL Worker binding is already declared in wrangler.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 deploy
The 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.
Paymug/paymug repository.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 |
UserGitRepo. The included workflow
declares contents: write, so its repository-scoped GITHUB_TOKEN can push
the merged update back to UserGitRepo without another token.After setup:
.github/workflows/sync-upstream.yml checks Paymug/paymug/latest every six
hours and can also be run manually. It safely merges new upstream commits and
pushes UserGitRepo/main./api/deployments exposes the current official Paymug version.
Settings → About → Check for update reads that endpoint, so no GitHub
token is stored in the Worker runtime.UserGitRepo from the Git remote; no repository
environment variable is required.UserGitRepo, then rerun Sync upstream updates..github/workflows/deploy.yml remains an optional manual fallback. It needs
CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_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.
See docs/README.md for developer, API, and AI-agent guides.
The documentation plan and completeness tracker are in
docs/DOCS.md.
38 commits
TypeScript
97.5%
CSS
2.3%