How I Built a System to Automatically Sell Access to a Private GitHub Repo
Source: Dev.to
TL;DR
I needed to sell access to a private GitHub repo. Zip downloads don’t get updates, license keys are overkill, and multiple payment providers rejected my business. I ended up building a fully automated system that handles the whole flow:
- Payment webhook fires → signature verified
- GitHub username captured via OAuth
- Collaborator invite sent in seconds
It works great, but it turns out Polar.sh already has this built‑in and does it better. I didn’t end up using my version, but I learned a ton building it.
The Problem
I built a boilerplate I wanted to sell. The product was done – the hard part was figuring out how to actually deliver it.
You build a starter kit, a boilerplate, a template, something developers would pay for. Now what? How do you actually get it to them after they pay?
Options I evaluated
| Option | Pros | Cons |
|---|---|---|
| Zip file download | Simple, works out‑of‑the‑box | No updates, no git history, no pull/diff |
| License‑key gating | Can lock access | Requires custom CLI + infra – overkill |
| npm private package | Familiar for devs | Requires registry auth → friction kills conversions |
| GitHub Sponsors (private repos) | Built‑in GitHub access | Subscription‑only, not good for one‑time purchases |
| Manual delivery via payment platform | Works for a few sales | Becomes impossible at scale (e.g., 3 sales at 2 am) |
What I actually wanted was dead simple: customer pays → customer gets access to the private repo automatically, instantly.
Payment‑Provider Roadblock
My registered business is a design and consulting firm. I tried signing up with Lemon Squeezy and a few other platforms to sell digital products, but they rejected me.
- I explained I was selling one‑time digital products (boilerplate code, starter kits), not consulting services.
- The rejection remained, and some providers were outright rude.
At that point I only knew I needed a provider that could fire a webhook on payment so I could automate the rest myself.
Inspiration
I found a project that did something similar:
- Payment webhook → GitHub collaborator invitation
- Private repo, read‑only access (clone + pull updates, full git history)
They charged $30 for it. I thought I could build it myself – two birds, one stone.
Why It Was Feasible
- GitHub API lets you add collaborators programmatically.
- Any payment provider that fires webhooks can trigger the flow.
- Permissions can be set to pull (read‑only) so customers can clone but can’t push.
My Solution Architecture
A stand‑alone Next.js app that is payment‑provider agnostic.
User clicks "Buy"
|
GitHub OAuth (capture username)
|
Payment checkout
|
Webhook fires → verify signature → invite to repo → send email
|
Customer has access in seconds
Main components
| Component | Description |
|---|---|
| GitHub OAuth | Captures the buyer’s GitHub username before checkout |
| Webhook handler | Receives payment confirmation, verifies HMAC‑SHA256 signature |
| GitHub API client | Sends the repository invitation automatically |
| PostgreSQL | Tracks every customer (33 fields of data) |
| Resend | Sends the welcome email |
Detailed Flow
1. Capture GitHub username before checkout
The payment provider doesn’t know the buyer’s GitHub username, so we must capture it first.
// After OAuth callback, redirect to checkout with GitHub info
const checkoutUrl = new URL(CHECKOUT_URL);
checkoutUrl.searchParams.set('gh_username', githubUser.login);
checkoutUrl.searchParams.set('gh_user_id', String(githubUser.id));
The payment provider includes these fields in the webhook payload, allowing us to know exactly who to invite when the payment completes.
2. Verify webhook signature
Anyone can POST to your webhook endpoint. Without verification an attacker could grant themselves free access.
import { createHmac, timingSafeEqual } from 'crypto';
function verifySignature(payload: string, signature: string): boolean {
const expected = createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Gotchas
- Use the raw request body, not the parsed JSON – re‑serialization changes whitespace and breaks the signature.
- Use
timingSafeEqualinstead of===– plain string comparison leaks timing information.
3. Upsert customer record
A customer may buy an upgrade or a second product. Without an UPSERT the second webhook fails with a unique‑constraint violation on their email.
INSERT INTO customers (email, name, amount_paid, ...)
VALUES (...)
ON CONFLICT (email) DO UPDATE SET
amount_paid = EXCLUDED.amount_paid,
order_id = EXCLUDED.order_id,
updated_at = NOW();
4. Handle pending invitations
If a customer doesn’t accept their invitation, it stays pending. After 50 pending invitations GitHub blocks new ones entirely.
- I track invitation status.
- I built an admin panel to monitor pending invites and manually resend or revoke them.
5. Retry on GitHub API failures
The GitHub API can go down, hit rate limits, or suffer network blips. A customer paying for your boilerplate shouldn’t just see an error and be told to contact support.
Exponential back‑off strategy
| Attempt | Delay |
|---|---|
| 1 | 1 second |
| 2 | 2 seconds |
| 3 | 4 seconds |
| 4 | 8 seconds |
| … | … |
| 10 | 1 hour |
Error classification
- Retryable – network timeouts, rate‑limit responses, 5xx errors → go through the back‑off loop.
- Non‑retryable – 4xx client errors (e.g., invalid payload) → log and alert.
Lessons Learned
- Payment providers can be finicky about the type of digital goods you sell.
- Signature verification must use the raw payload and a timing‑safe compare.
- UPSERT (or equivalent) prevents duplicate‑key errors when a buyer makes multiple purchases.
- Pending GitHub invites can silently block you – monitor and clean them up.
- Robust retry logic is essential when you depend on third‑party APIs.
Bottom Line
I built a fully automated, provider‑agnostic system that:
- Captures a buyer’s GitHub username via OAuth.
- Listens for a payment webhook, verifies it securely.
- Invites the buyer as a read‑only collaborator to a private repo.
- Sends a welcome email with the repo link.
It works, but Polar.sh already offers this functionality out‑of‑the‑box and does it better, so I ultimately switched to their service. Still, the experience gave me a deep dive into OAuth, webhook security, GitHub’s API, and reliable automation patterns.
Queue with Back‑off
- Permanent failures (e.g., user not found, repo not found,
401/403) → send to a dead‑letter queue for manual review. - After 10 failed attempts the item moves to the dead‑letter queue and I get notified.
GitHub Token Exchange Endpoint
The correct endpoint is https://github.com, not https://api.github.com.
# WRONG – returns an HTML login page
POST https://api.github.com/login/oauth/access_token
# CORRECT – returns the token
POST https://github.com/login/oauth/access_token
My muscle memory from using the GitHub API kept me pointing at the API sub‑domain, but the OAuth flow lives on the main site.
Stack Overview
| Component | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (strict mode) |
| Database | PostgreSQL on Neon |
| GitHub API | Octokit |
| Resend | |
| Validation | Zod |
| Testing | Vitest (all passing) |
Built to run on any hosting provider. Currently runs on Vercel’s free tier for low volume.
Why This Approach Has Real Advantages
- Instant updates – Push a fix to
main; every customer cangit pull– no need to re‑download zip files. - Full git history – Customers can see why things were built the way they were.
- Read‑only by default –
pullpermission lets them clone but not push to the repo. - No extra tooling – Only
gitis required; no license keys, custom CLIs, or registry auth. - Revocable access – Need to charge back? Remove the collaborator with a single API call.
Drawbacks
- 50 pending invitation limit – GitHub caps the number of outstanding invites.
- GitHub‑centric – Tied to GitHub; not a universal solution.
For selling developer tools to developers, the drawbacks are usually acceptable.
What I Ended Up Using Instead
While building the tool I switched to Polar.sh for payments because they accepted my business (unlike Lemon Squeezy). Their platform already handles everything I was recreating:
- GitHub username capture
- Repository invitations
- Access management
In short, Polar automates the exact flow I was trying to build:
- Customer pays.
- Polar captures their GitHub username.
- Polar sends the repo invitation.
- Customer gets access.
All I needed was approval, then I could flip the feature on.
Reflections
My tool works: all tests pass, the pipeline runs, but I never needed to deploy it. Still, I learned a ton:
- Webhook signature verification
- OAuth flows
- Retry systems with exponential back‑off
- How GitHub’s collaborator API actually works
These are the kinds of details you don’t get from tutorials.
Open Question
I built this because I saw someone charging $30 for a similar solution and thought I could do it myself.
- Would anyone actually find this useful?
- If you’re selling a boilerplate or starter kit and don’t want to be locked into Polar’s built‑in solution, would a standalone tool like this be worth it?
- Would you want it open‑sourced?
Let me know in the comments. I’m trying to decide whether to ship it (open source it / “buy me a coffee”) or keep it as a learning project. It feels a bit pointless to sell, but the build was fun.
TL;DR
Built with Next.js, TypeScript, and a lot of time reading webhook docs. 🚀