Skip to content

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
git add supabase_schema.sql
git commit -m "feat: add waitlist table for early access sign-ups"

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
git add tests/test_waitlist.py
git commit -m "test: add waitlist endpoint tests"

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:

from fastapi.middleware.cors import CORSMiddleware

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:

{"name": "Waitlist", "description": "Early access waitlist sign-up."},
  • [ ] 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
git add main.py
git commit -m "feat: add POST /waitlist endpoint with CORS for landing page"

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:

usnap.me

  • [ ] Step 7: Commit
git add landing/
git commit -m "feat: create landing page infrastructure from prototype"

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:

  1. Remove variant toggle: Delete the variant state (const [variant, setVariant] = useState('A')) and the toggle UI div. Always show Option A.

  2. Remove Option B entirely: Delete {variant === 'B' && (<OptionB go={go} />)}, the OptionB component definition, and the PlanPill highlight pill for Option B.

  3. Fix currency: Replace £19 / mo with $19 / mo in the PlanPill sub text.

  4. Fix webhook header: Replace X-Usnap-Signature with X-Webhook-Signature.

  5. Connect form to real API: Replace the simulated form submission:

Old (prototype):

setStateA('submitting');
setTimeout(() => setStateA('submitted'), 900);

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');
  });

  1. Add id to the select element: Add id="use-case-select" to the "What are you building?" <select>.

  2. Fix trust strip: Replace GDPR friendly · UK based with GDPR compliant.

  3. [ ] Step 3: Create pricing.jsx from pricing-docs.jsx

Read usnp.me prototype/pricing-docs.jsx. Create landing/pricing.jsx with these changes:

  1. Fix all currency: Replace every £0 with $0 and every £19 with $19.

  2. Keep Docs component: The Docs component shows brand/theme guidance and links to the external docs. Keep it, but fix the API references:

  3. Replace Authorization: Bearer $KEY with X-API-Key: $KEY in the docs preview code snippet
  4. Replace "url": with "redirect_url": in the docs preview code snippet

  5. Fix payment methods FAQ: Replace SEPA, BACS Direct Debit and bank transfer with ACH bank transfer (USD context).

  6. Update Object.assign: Ensure it exports both Pricing and Docs: Object.assign(window, { Pricing, Docs });

  7. [ ] 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%.