Issues #9, #11, #12 Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add a public waitlist endpoint, deploy the marketing landing page adapted from the designer's prototype, and write the developer community launch plan.
Architecture: Issue #12 adds a POST /waitlist public endpoint (no auth required) to the FastAPI app with CORS for cross-origin form submissions from the marketing site. Issue #9 adapts the designer's React prototype (in usnp.me prototype/) into a deployable static site in landing/, fixing API contract mismatches and switching GBP to USD. Issue #11 produces a launch content document with ready-to-post content for HN, Reddit, and dev communities.
Tech Stack: FastAPI, Supabase, React (CDN + Babel), GitHub Pages
File Map¶
Issue #12 (Waitlist endpoint):
- Modify: supabase_schema.sql — add waitlist table DDL
- Modify: main.py — add WaitlistEntry model, POST /waitlist endpoint, CORS middleware
- Create: tests/test_waitlist.py — endpoint tests
Issue #9 (Landing page):
- Create: landing/index.html — adapted from usnp.me prototype/usnap.me.html
- Create: landing/styles.css — copied from usnp.me prototype/styles.css
- Create: landing/components.jsx — adapted from usnp.me prototype/components.jsx
- Create: landing/app.jsx — adapted from usnp.me prototype/app.jsx
- Create: landing/landing.jsx — adapted from usnp.me prototype/landing.jsx
- Create: landing/signup.jsx — adapted from usnp.me prototype/signup.jsx
- Create: landing/pricing.jsx — adapted from usnp.me prototype/pricing-docs.jsx
- Create: landing/CNAME — GitHub Pages custom domain
Issue #11 (Launch plan):
- Create: docs/launch-plan.md — launch content for all channels
Content Corrections Summary¶
These corrections apply across all landing page JSX files. The designer's prototype had a few mismatches with the actual API:
| Prototype | Actual API | Files affected |
|---|---|---|
Authorization: Bearer $KEY |
X-API-Key: $KEY |
landing.jsx code samples |
"url": / url: |
"redirect_url": / redirect_url: |
landing.jsx code samples |
X-Usnap-Signature |
X-Webhook-Signature |
landing.jsx, signup.jsx |
£0, £19 |
$0, $19 |
landing.jsx, signup.jsx, pricing.jsx |
Task 1: Add waitlist table to Supabase¶
Files:
- Modify: supabase_schema.sql
Issue: #12
- [ ] Step 1: Append waitlist table DDL to schema file
Add to the end of supabase_schema.sql:
-- Waitlist: early access sign-ups
CREATE TABLE waitlist (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email TEXT NOT NULL UNIQUE,
use_case TEXT,
plan_interest TEXT NOT NULL DEFAULT 'free',
created_at TIMESTAMPTZ DEFAULT NOW()
);
ALTER TABLE waitlist ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow all operations on waitlist" ON waitlist FOR ALL USING (true) WITH CHECK (true);
- [ ] Step 2: Apply migration to Supabase
Use the Supabase MCP tool apply_migration with:
- name: add_waitlist_table
- query: the SQL from step 1
- [ ] Step 3: Commit
Task 2: Write waitlist endpoint tests¶
Files:
- Create: tests/test_waitlist.py
Issue: #12
- [ ] Step 1: Write tests for POST /waitlist
Create tests/test_waitlist.py. This test file follows the existing pattern in the codebase: use test_client and mock_supabase fixtures from conftest.py.
"""Tests for the waitlist sign-up endpoint."""
from unittest.mock import MagicMock
import pytest
class TestJoinWaitlist:
"""Tests for POST /waitlist."""
def test_join_waitlist_basic(self, test_client, mock_supabase):
"""Successful waitlist submission with just email."""
mock_supabase.set_response("waitlist", [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "[email protected]",
"use_case": None,
"plan_interest": "free",
"created_at": "2026-05-13T10:00:00Z",
}
])
response = test_client.post(
"/waitlist",
json={"email": "[email protected]"},
)
assert response.status_code == 201
body = response.json()
assert body["email"] == "[email protected]"
assert body["plan_interest"] == "free"
assert "message" in body
def test_join_waitlist_full(self, test_client, mock_supabase):
"""Waitlist submission with all optional fields."""
mock_supabase.set_response("waitlist", [
{
"id": "550e8400-e29b-41d4-a716-446655440001",
"email": "[email protected]",
"use_case": "Product packaging / QR codes",
"plan_interest": "pro",
"created_at": "2026-05-13T10:00:00Z",
}
])
response = test_client.post(
"/waitlist",
json={
"email": "[email protected]",
"use_case": "Product packaging / QR codes",
"plan_interest": "pro",
},
)
assert response.status_code == 201
body = response.json()
assert body["use_case"] == "Product packaging / QR codes"
assert body["plan_interest"] == "pro"
def test_join_waitlist_invalid_email(self, test_client, mock_supabase):
"""Rejects invalid email format."""
response = test_client.post(
"/waitlist",
json={"email": "not-an-email"},
)
assert response.status_code == 422
def test_join_waitlist_missing_email(self, test_client, mock_supabase):
"""Rejects request without email."""
response = test_client.post(
"/waitlist",
json={"use_case": "Testing"},
)
assert response.status_code == 422
def test_join_waitlist_invalid_plan(self, test_client, mock_supabase):
"""Rejects invalid plan_interest value."""
response = test_client.post(
"/waitlist",
json={"email": "[email protected]", "plan_interest": "enterprise"},
)
assert response.status_code == 422
def test_join_waitlist_duplicate_email(self, test_client, mock_supabase):
"""Returns 409 for duplicate email."""
mock_table = MagicMock()
mock_table.insert.return_value = mock_table
mock_table.execute.side_effect = Exception(
"duplicate key value violates unique constraint"
)
mock_supabase.table = MagicMock(return_value=mock_table)
response = test_client.post(
"/waitlist",
json={"email": "[email protected]"},
)
assert response.status_code == 409
assert "already" in response.json()["detail"].lower()
- [ ] Step 2: Run tests to verify they fail
Run: pytest tests/test_waitlist.py -v
Expected: FAIL — endpoint does not exist (405 Method Not Allowed or 404)
- [ ] Step 3: Commit
Task 3: Implement waitlist endpoint with CORS¶
Files:
- Modify: main.py
Issue: #12
- [ ] Step 1: Add CORS middleware
Add import at the top of main.py with the other FastAPI imports:
After the line app.add_exception_handler(RateLimitExceeded, _custom_rate_limit_exceeded_handler) (around line 142), add:
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://usnap.me",
"http://localhost:3000",
"http://localhost:8080",
],
allow_methods=["POST", "OPTIONS"],
allow_headers=["Content-Type"],
)
- [ ] Step 2: Add Waitlist tag to OpenAPI tags
In the openapi_tags list (around line 119-125), add:
- [ ] Step 3: Add WaitlistEntry model
After the QRFormat enum (around line 147), add:
class WaitlistEntry(BaseModel):
email: str
use_case: Optional[str] = None
plan_interest: str = "free"
@model_validator(mode="after")
def validate_fields(self):
if not re.match(r"^\S+@\S+\.\S+$", self.email):
raise ValueError("Invalid email address")
if self.plan_interest not in ("free", "pro"):
raise ValueError("plan_interest must be 'free' or 'pro'")
return self
Note: re is already imported at the top of main.py.
- [ ] Step 4: Add the waitlist endpoint
After the read_root health check endpoint (around line 554), add:
@app.post(
"/waitlist",
status_code=201,
tags=["Waitlist"],
summary="Join the early access waitlist",
responses={
409: {"description": "Email already on waitlist"},
422: {"description": "Invalid email or plan"},
},
)
@limiter.limit("5/minute")
def join_waitlist(request: Request, entry: WaitlistEntry):
"""
Submit your email to join the early access waitlist.
No authentication required. We'll email your API key within 24 hours.
"""
try:
result = supabase.table("waitlist").insert({
"email": entry.email,
"use_case": entry.use_case,
"plan_interest": entry.plan_interest,
}).execute()
except Exception as e:
if "duplicate" in str(e).lower() or "unique" in str(e).lower():
raise HTTPException(
status_code=409, detail="This email is already on the waitlist"
)
raise
if not result.data:
raise HTTPException(status_code=500, detail="Failed to join waitlist")
record = result.data[0]
return {
"email": record["email"],
"use_case": record.get("use_case"),
"plan_interest": record["plan_interest"],
"message": "You're on the list! We'll email your API key within 24 hours.",
}
- [ ] Step 5: Run waitlist tests
Run: pytest tests/test_waitlist.py -v
Expected: All 6 tests PASS
- [ ] Step 6: Run full test suite
Run: pytest -v
Expected: All tests PASS (111 existing + 6 new = 117)
- [ ] Step 7: Commit
Task 4: Create landing page — infrastructure files¶
Files:
- Create: landing/index.html
- Create: landing/styles.css
- Create: landing/components.jsx
- Create: landing/app.jsx
- Create: landing/CNAME
Issue: #9
Context: The designer's prototype lives in usnp.me prototype/. We're adapting it into a production landing/ directory, removing designer tooling (tweaks panel) and the dashboard (will be a separate app), and fixing the content corrections listed in the summary table above.
- [ ] Step 1: Read the prototype HTML file
Read usnp.me prototype/usnap.me.html to understand the structure.
- [ ] Step 2: Create landing/index.html
Create landing/index.html based on the prototype HTML with these changes:
- Remove the <script src="dashboard.jsx" type="text/babel"> tag
- Remove the <script src="tweaks-panel.jsx" type="text/babel"> tag
- Keep all other script tags: components.jsx, landing.jsx, signup.jsx, pricing-docs.jsx (renamed to pricing.jsx), app.jsx
- Change the pricing-docs.jsx reference to pricing.jsx
- Set <title> to usnap.me — Shortlinks that carry your data
- [ ] Step 3: Copy styles.css
Copy usnp.me prototype/styles.css to landing/styles.css with no changes.
- [ ] Step 4: Create components.jsx
Read usnp.me prototype/components.jsx. Copy to landing/components.jsx with these changes:
In the Nav component, remove Dashboard from the links array. The result should be:
const links = [
{ id: 'landing', label: 'Home' },
{ id: 'pricing', label: 'Pricing' },
{ id: 'docs', label: 'Docs' },
];
In the Footer component:
- In the Product column, change <a onClick={() => go('dashboard')}>Dashboard</a> to <a onClick={() => go && go('signup')}>Get API key</a>
- In the Developers column, change the Swagger UI link to: <a href="https://usnp.me/docs" target="_blank" rel="noreferrer">Swagger UI</a>
- [ ] Step 5: Create app.jsx
Important: The prototype's app.jsx uses useTweaks() which is defined in tweaks-panel.jsx — a file we are NOT including. Do NOT copy the prototype's app.jsx. Write landing/app.jsx from scratch with this exact content:
/* Landing site app — no dashboard, no tweaks panel */
const { useState, useEffect } = React;
function App() {
const [route, setRoute] = useState(() => {
const h = (location.hash || '').slice(1);
return ['landing', 'signup', 'pricing', 'docs'].includes(h) ? h : 'landing';
});
useEffect(() => {
location.hash = route;
window.scrollTo({top: 0, behavior: 'instant'});
}, [route]);
useEffect(() => {
document.documentElement.dataset.theme = 'light';
document.documentElement.style.setProperty('--wonky', '1.2deg');
}, []);
const go = (id) => setRoute(id);
return (
<div data-screen-label={`usnap.me — ${route}`}>
<Nav route={route} go={go}/>
<div className="page">
{route === 'landing' && <Landing go={go}/>}
{route === 'signup' && <SignUp go={go}/>}
{route === 'pricing' && <Pricing go={go}/>}
{route === 'docs' && <Docs go={go}/>}
</div>
<Footer go={go}/>
</div>
);
}
ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
- [ ] Step 6: Create CNAME
Create landing/CNAME:
- [ ] Step 7: Commit
Task 5: Fix landing page content — API refs, USD, waitlist integration¶
Files:
- Create: landing/landing.jsx
- Create: landing/signup.jsx
- Create: landing/pricing.jsx
Issue: #9
- [ ] Step 1: Create landing.jsx with corrected API references
Read usnp.me prototype/landing.jsx. Create landing/landing.jsx with these specific find-and-replace changes:
Code samples (the codeSamples object, 3 language variants):
| Find | Replace |
|---|---|
-H "Authorization: Bearer $USNAP_KEY" |
-H "X-API-Key: $USNAP_KEY" |
headers={"Authorization": f"Bearer {os.environ['USNAP_KEY']}"} |
headers={"X-API-Key": os.environ["USNAP_KEY"]} |
Authorization: "Bearer " + process.env.USNAP_KEY, |
"X-API-Key": process.env.USNAP_KEY, |
"url": "https://your-product.example/box/A-12" (in cURL -d) |
"redirect_url": "https://your-product.example/box/A-12" |
"url": "https://your-product.example/box/A-12" (in Python json=) |
"redirect_url": "https://your-product.example/box/A-12" |
url: "https://your-product.example/box/A-12" (in Node body) |
redirect_url: "https://your-product.example/box/A-12" |
Step visuals:
| Find | Replace |
|------|---------|
| { "url": "...", "data": {…} } | { "redirect_url": "...", "data": {…} } |
| X-Usnap-Signature: t=…, v1=… | X-Webhook-Signature: sha256=… |
Pricing preview section:
| Find | Replace |
|------|---------|
| Then £19. | Then $19. |
PricingCards component (inside landing.jsx):
| Find | Replace |
|------|---------|
| £0 | $0 |
| £19 | $19 |
Dev love bar:
| Find | Replace |
|------|---------|
| X-Usnap-Signature (in the <code> tag and FeatureRow) | X-Webhook-Signature |
- [ ] Step 2: Create signup.jsx with waitlist-only flow and API integration
Read usnp.me prototype/signup.jsx. Create landing/signup.jsx with these changes:
-
Remove variant toggle: Delete the variant state (
const [variant, setVariant] = useState('A')) and the toggle UI div. Always show Option A. -
Remove Option B entirely: Delete
{variant === 'B' && (<OptionB go={go} />)}, theOptionBcomponent definition, and thePlanPillhighlight pill for Option B. -
Fix currency: Replace
£19 / mowith$19 / moin the PlanPill sub text. -
Fix webhook header: Replace
X-Usnap-SignaturewithX-Webhook-Signature. -
Connect form to real API: Replace the simulated form submission:
Old (prototype):
New (production):
setErrorA('');
setStateA('submitting');
fetch("https://usnp.me/waitlist", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: emailA,
use_case: document.querySelector('#use-case-select')?.value || undefined,
plan_interest: tier,
}),
})
.then(r => {
if (r.status === 409) {
setErrorA("You're already on the list!");
setStateA('idle');
return;
}
if (!r.ok) throw new Error('Failed');
setStateA('submitted');
})
.catch(() => {
setErrorA("Something went wrong. Please try again.");
setStateA('idle');
});
-
Add id to the select element: Add
id="use-case-select"to the "What are you building?"<select>. -
Fix trust strip: Replace
GDPR friendly · UK basedwithGDPR compliant. -
[ ] Step 3: Create pricing.jsx from pricing-docs.jsx
Read usnp.me prototype/pricing-docs.jsx. Create landing/pricing.jsx with these changes:
-
Fix all currency: Replace every
£0with$0and every£19with$19. -
Keep Docs component: The Docs component shows brand/theme guidance and links to the external docs. Keep it, but fix the API references:
- Replace
Authorization: Bearer $KEYwithX-API-Key: $KEYin the docs preview code snippet -
Replace
"url":with"redirect_url":in the docs preview code snippet -
Fix payment methods FAQ: Replace
SEPA, BACS Direct Debit and bank transferwithACH bank transfer(USD context). -
Update Object.assign: Ensure it exports both
PricingandDocs:Object.assign(window, { Pricing, Docs }); -
[ ] Step 4: Verify landing page loads
Open landing/index.html in a browser (or serve with python -m http.server 3000 --directory landing/) and verify:
- Landing page loads without console errors
- All code samples show X-API-Key (not Authorization: Bearer)
- All code samples show redirect_url (not url)
- All prices show $ (not £)
- Dev love bar shows X-Webhook-Signature (not X-Usnap-Signature)
- Sign-up page shows waitlist form (no Option A/B toggle)
- Pricing page shows $0 and $19
- Navigation works between all pages (landing, signup, pricing, docs)
- [ ] Step 5: Commit
git add landing/landing.jsx landing/signup.jsx landing/pricing.jsx
git commit -m "feat: adapt landing page content — fix API refs, USD pricing, waitlist form"
Task 6: Write developer community launch plan¶
Files:
- Create: docs/launch-plan.md
Issue: #11
- [ ] Step 1: Write the launch plan document
Create docs/launch-plan.md with ready-to-post content for each channel. The document should include:
1. Positioning (at the top) Lead with the use case, not "URL shortener": - Primary: "Turn any QR code into a webhook trigger with custom data" - Secondary: "The missing link between physical events and your API" - Tertiary: "Shortlinks that carry context to your webhooks"
2. Hacker News — Show HN post
Title: Show HN: usnap.me – Shortlinks that carry JSON data to your webhooks
Body (draft the full text, ~200 words): - What it does: Create a shortlink with a JSON payload and webhook URLs. Every click/scan fires your webhooks with the data + HMAC signature. - Why: Connects physical events (QR scans on product packaging, flyers, name badges) to your code. No queue to set up, no server to run. - Tech: FastAPI, Supabase, 5 endpoints, OpenAPI docs. Free tier: 100 links/mo, 1000 webhook deliveries. Pro: $19/mo unlimited. - Links: API docs, Swagger UI, landing page - Ask: "What would you build with it?"
3. Reddit posts (draft titles + body for each sub)
- r/SideProject: Project story angle — "I built a webhook-powered URL shortener for connecting physical stuff to APIs"
- r/webdev: Technical angle — "I built an API that turns shortlinks into webhook triggers — here's the architecture"
- r/selfhosted: Self-hosting angle (if applicable, or skip if not open-source yet)
- r/InternetIsBeautiful: Short angle — "usnap.me — QR codes that fire webhooks with custom JSON data"
4. Dev Twitter/Bluesky thread (draft 5-7 tweet-sized posts)
Thread structure: 1. Hook: "I built an API that makes QR codes fire webhooks." 2. The problem 3. How it works (3-step) 4. Code example (screenshot or text) 5. What people are building with it 6. Free tier details + link
5. Indie Hackers post (draft ~300 words)
Business story: Why this exists, who it's for, early traction, what's next.
6. Dev.to / Hashnode tutorial (outline + intro)
Title: "How to build a QR-to-webhook pipeline in 5 minutes with usnap.me"
Outline the full tutorial: install nothing, create a shortlink, set up a webhook receiver, scan the QR code, see the payload. Include a complete working example.
7. Pre-launch checklist
- [ ] Landing page live at usnap.me
- [ ] Swagger UI accessible at usnp.me/docs
- [ ] Developer docs at granteagon.github.io/usnap.me
- [ ] Rate limiting tested under load
- [ ] Sentry monitoring active
- [ ] Waitlist endpoint working
- [ ] Example webhook receiver deployed for live demo
-
[ ] Email collection working (waitlist table)
-
[ ] Step 2: Commit
git add docs/launch-plan.md
git commit -m "docs: add developer community launch plan with channel content"
Task 7: Final verification¶
Issue: All
- [ ] Step 1: Run full test suite
Run: pytest -v --tb=short
Expected: All tests pass (111 existing + 6 waitlist = 117)
- [ ] Step 2: Verify landing page
Serve: python -m http.server 3000 --directory landing/
Check:
- All 4 routes work (landing, signup, pricing, docs)
- Code samples show correct API contract (X-API-Key, redirect_url, X-Webhook-Signature)
- All prices in USD ($0, $19)
- Waitlist form attempts to POST to https://usnp.me/waitlist (will fail locally due to CORS, but the fetch call should be visible in Network tab)
- No JavaScript console errors
- [ ] Step 3: Verify API docs updated
Run: curl http://localhost:8000/openapi.json | python -m json.tool | grep -A2 waitlist
Expected: The /waitlist endpoint appears in the OpenAPI spec with the "Waitlist" tag.
- [ ] Step 4: Check no regressions
Run: pytest --cov=. --cov-report=term-missing
Expected: Coverage stays above 95%.