Pluggable action-gated access for any stack: verify an action, email a one-time code, issue a signed JWT.
Project description
Gated Access
Pluggable action-gated access for any stack.
Require a verifiable action — star a repo, follow a user, or your own custom check — then email a one-time code and issue a signed access token.
FastAPI · zero-setup local demo · swap an adapter, not your project
The idea
Plenty of products want to gate access behind an action: "do this, then you're in." The fragile way is to hard-wire that one action into your app. When the platform or the rules change, you rewrite everything.
Gated Access treats the action as a swappable adapter. The engine only knows the flow:
┌──────────┐ ┌───────────┐ ┌──────────────┐ ┌─────────────┐
│ Identify │ -> │ Action │ -> │ One-time │ -> │ Access │
│ (OAuth) │ │ (verify) │ │ code (mail) │ │ (JWT) │
└──────────┘ └───────────┘ └──────────────┘ └─────────────┘
The user signs in, Gated Access checks the action against their real account, emails a single-use code, and exchanges that code for a JWT your app can verify with a shared secret. Identity, verification, email, and storage are each an adapter — change one and nothing else moves.
"Any stack" means the engine is one small headless HTTP API plus a drop-in frontend. Your app can be anything; it just talks to four endpoints (or verifies the JWT directly).
Quickstart — the local demo
No GitHub app, no email provider, no credit card. It runs entirely on your machine.
pip install gated-access
gated-access run # open http://localhost:8000
Click through all four steps. In demo mode Gated Access:
- simulates the GitHub sign-in (no real OAuth needed),
- lets you toggle "I did the action" to flip what the verifier sees,
- prints the email to the console and shows it in an on-screen demo inbox so you can read the code without sending real mail.
CLI
| Command | What it does |
|---|---|
gated-access run [--host H] [--port P] [--config PATH] |
Start the server. Host/port default to the config values; --config points at an alternate gated-access.yaml. |
gated-access secrets |
Print ready-to-paste GATED_ACCESS_JWT_SECRET= / GATED_ACCESS_SESSION_SECRET= lines (cryptographically random). |
gated-access init [--force] |
Interactively write a commented gated-access.yaml in the current directory. Pressing Enter at every prompt yields the demo config; non-demo choices print the env vars you still need to set. |
gated-access version |
Print the installed version. |
python -m gated_access also starts the server, and uvicorn gated_access.main:app remains the
plain ASGI entrypoint for deployment.
How it fits together
Four adapter families, each behind a tiny interface in gated_access/adapters/base.py:
| Adapter | Demo default | Production options |
|---|---|---|
| Identity — who is this user? | demo (fabricated user) |
github (OAuth web flow) |
| Verification — did they do it? | manual (simulated) |
github_star, github_follow |
| Email — deliver the code | console (+ demo inbox) |
resend, smtp |
| Storage — sessions & codes | memory |
sqlite |
Adding a new check (a webhook, an on-chain event, "is a paying customer") is just a new
VerificationAdapter subclass — the routes, codes, tokens, and UI stay exactly the same.
Configuration
Everything is driven by gated-access.yaml, with environment variables overriding the file
so secrets never live in config. Common keys:
| Key | Meaning |
|---|---|
identity.provider |
demo or github |
verify.type |
manual · github_star (needs verify.repo) · github_follow (needs verify.username) |
email.provider |
console · resend · smtp |
storage.backend |
memory · sqlite |
code.length / code.ttl_seconds / code.single_use |
one-time code policy |
access_ttl_seconds |
JWT lifetime |
demo_mode |
enables the simulated login + /dev/* endpoints — set false in production |
branding.* |
product name, copy, and accent colour shown in the UI |
See .env.example for the full list of secret/deploy variables.
HTTP API
The frontend is optional — these are all you need to integrate from any language.
| Method & path | Purpose |
|---|---|
GET /auth/login |
Begin sign-in (redirects to the identity provider) |
GET /auth/callback |
Finish sign-in; sets a signed, http-only session cookie |
GET /api/session |
Current status: authenticated? action done? verified? |
POST /api/verify |
Run the verification adapter; on success, email a one-time code |
POST /api/redeem |
Body {"code": "..."} → {"access": true, "token": "<JWT>"} |
GET /api/me |
Example protected resource — requires Authorization: Bearer <JWT> |
GET /healthz |
Liveness check |
POST /dev/perform-action, GET /dev/inbox |
Demo-only helpers (disabled when demo_mode: false) |
Your backend doesn't even have to call /api/me — verify the JWT yourself with the shared
jwt_secret (HS256, issuer gated-access).
Going to production
- Generate secrets and set them in the environment:
gated-access secrets # prints GATED_ACCESS_JWT_SECRET=... and GATED_ACCESS_SESSION_SECRET=...
Gated Access refuses to start withdemo_mode: falsewhile the dev placeholder secrets are still in place. - Create a GitHub OAuth app at https://github.com/settings/developers. Set the
callback URL to
https://your-domain/auth/callbackand exportGITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,GITHUB_REDIRECT_URI. - Switch the adapters in
gated-access.yaml:demo_mode: false identity: { provider: github } verify: { type: github_star, repo: "you/your-repo" } email: { provider: resend } # set RESEND_API_KEY in env storage: { backend: sqlite }
- Serve it behind HTTPS (the session cookie is marked
Secureautomatically whenbase_urlishttps):uvicorn gated_access.main:app --host 0.0.0.0 --port 8000
Because it's a plain ASGI app with no required cloud services, it deploys anywhere that runs Python — a container, Render, Railway, Fly, a VM, etc.
Security notes
- One-time codes drawn from an unambiguous alphabet (no
0/O/1/I/L), single-use and time-limited, and bound to the session that earned them — a code minted for one user is rejected for another. - JWT access tokens (HS256) with issuer and expiry; verify with your shared secret.
- OAuth
stateis single-use and short-lived (CSRF protection); the session cookie is signed (itsdangerous), http-only, andSecureover HTTPS. - The provider access token is held only until verification succeeds, then dropped.
/api/redeemis rate-limited per session + IP to blunt code-guessing.
A note on GitHub's rules
GitHub's Acceptable Use Policies prohibit incentivizing stars ("inauthentic interactions" and giving stars in exchange for any benefit). So a real "star my repo to unlock" gate aimed at the public would violate those terms.
This project is built as a portfolio / reference demo of the pattern, not a growth
hack. Keep it on the right side of the line by pointing the verifier at your own repo for
testing, or by using a check that isn't incentivized starring — github_follow, a webhook,
"is a customer", or any custom VerificationAdapter you write. The whole point of the
adapter design is that the gate survives exactly this kind of rule change.
Project layout
gated_access/
├── gated_access/
│ ├── adapters/ identity · verification · email · storage (+ base interfaces)
│ ├── service.py the engine: begin_auth → verify → redeem
│ ├── factory.py builds adapters from config and wires the FastAPI routes
│ ├── security.py signed cookies, OAuth state, rate limiter
│ ├── tokens.py one-time codes + JWT
│ ├── config.py typed config (YAML + env)
│ ├── cli.py the `gated-access` command (run · secrets · init · version)
│ ├── _version.py single source of truth for the version
│ ├── templates/ demo UI
│ └── static/ styles, client logic, logo
├── tests/ token unit tests + full end-to-end flow
├── pyproject.toml packaging (hatchling) — the source of truth for dependencies
├── gated-access.yaml configuration (demo defaults)
├── .env.example production secrets template
└── requirements*.txt convenience for clone-based development
Development
git clone https://github.com/NI3singh/gated-access && cd gated-access
pip install -e .[dev]
pytest -q
gated-access run # or: python -m gated_access
Publishing (maintainers)
python -m build # dist/gated-access-X.Y.Z.tar.gz + .whl
twine check dist/*
twine upload --repository testpypi dist/* # dress rehearsal
pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple gated-access
twine upload dist/* # real release (needs a PyPI API token)
The version lives in gated_access/_version.py only — bump it there.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gated_access-1.0.1.tar.gz.
File metadata
- Download URL: gated_access-1.0.1.tar.gz
- Upload date:
- Size: 30.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
284198a4e4c09508e45fffa19ec504ff04503a23dcc7195ccc1bf00f5dbec67a
|
|
| MD5 |
9ec7a722290d6c311e451abdfc32aa92
|
|
| BLAKE2b-256 |
502f8a7e810e7952aa823d5b361ea96cbf166c15b28607a5ec08dd717ada9047
|
File details
Details for the file gated_access-1.0.1-py3-none-any.whl.
File metadata
- Download URL: gated_access-1.0.1-py3-none-any.whl
- Upload date:
- Size: 35.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe7875bec1abc3a13b746c6a40cdfe987c10161d5b475d4129e198889782c8eb
|
|
| MD5 |
e94eb82f020c3ef0fe6c92f0cadd59da
|
|
| BLAKE2b-256 |
208af91e06f50e0f51915c5d1daf04a35b61a5725df083b35c2ea03d37511c1b
|