This is a build log. Not a post-mortem, not a "lessons learned" retrospective cleaned up three months later. I'm writing this the same week the product shipped, while the decisions are still fresh enough to be embarrassing.

The product is ticketyboo: a VS Code extension that uses a devcontract.json to brief an AI on your codebase before it helps you write code. The contract captures stack, security policies, quality thresholds, and definition of done. The AI reads it. You get completions that know your rules. You pay per use.

The build took one month. Here's exactly what happened.

The constraint that shaped everything

AWS free tier. Not "try to stay on free tier" — hard constraint, non-negotiable. Every infrastructure decision ran through the same filter first: does this cost money?

This ruled out, immediately: NAT gateways ($32/month), RDS ($13+/month for db.t3.micro after 12 months), Secrets Manager ($0.40/secret/month), WAF ($5/month per web ACL), KMS customer-managed keys ($1/month each), Elastic IPs left floating.

What's left is actually a complete production stack:

CloudFront + S3          → static site (always free tier)
API Gateway + Lambda     → backend API (1M requests free/month)
DynamoDB on-demand       → database (25 GB free, 200M requests)
Cognito Essentials       → auth (50,000 MAU free, always)
SES from Lambda          → email (3,000/month free)
SSM Parameter Store      → secrets (free, unlike Secrets Manager)
Route 53                 → DNS ($0.50/month — required for ALIAS at apex, accepted)

The only ongoing cost is the Route 53 hosted zone. Everything else is free until meaningful scale.

The architecture: one Lambda, one table

The API is a single Lambda function in Python 3.12. All routing happens inside it — a manual router, not a framework. The handler is about 2,900 lines. I know how that sounds. Here is why it's fine: the Lambda cold-start is a solved problem (provisioned concurrency exists when needed), the routing is a flat if/elif chain that's trivially readable, and the alternative — multiple Lambdas behind a microservices boundary — adds operational overhead with no benefit at this traffic level.

The database is a single DynamoDB table with a pk/sk composite key and two GSIs: one for API key lookup (key_hash), one for Stripe customer lookup (stripe_customer_id). All user records, balance records, API key records, subscription records, and usage records share the same table. Access patterns are explicit. There's no ORM. DynamoDB single-table design forces you to think about access patterns up front. That discipline pays off.

Auth: two paths, one _require_auth()

The extension authenticates with tbo- prefixed API keys — long-lived tokens stored in DynamoDB. The web account portal authenticates with Cognito JWTs via PKCE. Both paths go through a single _require_auth() function:

def _require_auth(event: dict) -> tuple[Optional[dict], Optional[dict]]:
    # Fast path: tbo- API key (extension users)
    api_key = event.get("headers", {}).get("x-api-key", "")
    if api_key.startswith("tbo-"):
        user = auth.validate_api_key(api_key)
        if user:
            return user, None
        return None, _error_response(401, "unauthorized", "Invalid API key")

    # Cognito path: Bearer JWT (web portal users)
    token = auth.extract_bearer_token(event)
    if token:
        claims = auth.validate_cognito_jwt(token)
        if claims:
            user = auth.get_or_create_cognito_user(claims)
            if user:
                return user, None
        return None, _error_response(401, "unauthorized", "Invalid token")

    return None, _error_response(401, "unauthorized", "Authentication required")

The Cognito path has one non-obvious gotcha: Cognito access tokens don't have an aud claim. They have client_id instead. PyJWT will fail audience validation if you use the standard audience= parameter. The fix is options={"verify_aud": False} and a manual check on claims.get("client_id"). This cost two hours. It's not documented prominently anywhere.

Auto-provisioning on first JWT login means no separate sign-up endpoint for Cognito users. The get_or_create_cognito_user() function uses the Cognito sub as the user ID, calls db.create_user(), and seeds 10 free credits via billing.seed_free_tier_credits(). First login is seamless. There's no "account already exists" error to handle because we check first.

Billing: Stripe + DynamoDB conditional writes

Stripe sends webhook events. Webhooks can arrive more than once. The standard advice is "check if you've already processed the event ID." The standard implementation is a database write with a uniqueness constraint.

DynamoDB doesn't have unique constraints. It has conditional writes. The claim_stripe_event() function does a put_item with ConditionExpression="attribute_not_exists(pk)". If the item already exists, DynamoDB raises ConditionalCheckFailedException. That's the idempotency guard.

Credit pricing is explicit: 1 credit = $1. Plans are Starter ($8 = 8 credits/month), Pro ($20 = 20 credits/month), Business ($40 = 40 credits/month). Top-up packs are $5 for 5 credits, never expire. No credit ambiguity. Users know exactly what they're buying.

The extension: Brief Wizard

The VS Code extension has one command: ticketyboo: Open Brief Wizard. It opens a webview, walks through a series of questions about your project, and writes a devcontract.json to your workspace root. From that point, completions use the contract as context.

The extension is TypeScript, compiled to out/, packaged with vsce. No webpack, no bundler, no build step beyond tsc. The packaged VSIX is 3.86 KB. That's the entire extension.

Configuration is via VS Code settings: ticketyboo.apiBase (defaults to https://api.ticketyboo.dev) and ticketyboo.apiKey (your tbo- key). Enterprise teams can point apiBase at a self-hosted instance.

Testing: 473 tests, zero mocks of production behaviour

The test suite grew from 68 tests (Sprint 1) to 473 tests by launch. It covers: unit tests for every DB access pattern, handler tests for every HTTP route, billing tests with Stripe mocked at the right abstraction level, auth tests for both API key and JWT paths, linter tests for every contract rule, export tests for JSONL and CSV formats, rate limiter tests, webhook tests.

The discipline: never mock production behaviour. If DynamoDB raises ConditionalCheckFailedException, the test should trigger that actual exception, not a generic mock. If the Cognito JWT has no aud claim, the test should validate against a real JWT structure, not a stub.

Tests caught four real bugs during development:

AI-assisted development: what worked, what didn't

Roo (Claude Sonnet, running as a VS Code extension) wrote the majority of the code in this project. That sentence requires unpacking.

"Wrote" means: given a clear specification and a correct test, Roo produced working code on the first or second attempt for roughly 80% of the implementation. The remaining 20% required iteration — usually because the specification wasn't precise enough, not because the AI made an error.

What worked well:

What didn't work well:

The product itself — the devcontract.json Brief Wizard, the credit system, the account portal — exists precisely because these limitations are solvable. Embed the constraints in the contract. Review the output. Ship.

What's next

The VS Code extension is publishing to the Marketplace this week. The account portal is live. The API is on the free tier and stays there until the traffic warrants otherwise.

The next phase: a governance agent that runs on pull requests, reads the devcontract, and flags deviations before merge. The Lambda scaffolding for that is already in the codebase. The SQS queue is configured. It's a matter of writing the agent logic.

If you want to try it: the extension is free for the first 10 completions per month. No card required. The Brief Wizard takes about three minutes.

Try ticketyboo DevContract Gate VS Code extension — 10 free completions/month — no card required
Get the extension → See pricing

Try ticketyboo on your codebase

Scan a public GitHub repository for dependency health, licence compliance, IaC quality, and secret exposure. Free, no account required.

Scan your repo Install the VS Code extension