# Deploy a FastAPI polling API with PostgreSQL on Yeeted

A Yeeted sample: polls and voting. What it demonstrates:

- **FastAPI + SQLAlchemy + alembic**: the platform detects `alembic.ini` and
  runs `alembic upgrade head` at release against the injected `DATABASE_URL`,
  so the schema (two revisions here) is migrated before the app serves.
- **Correctness lives in the database**: one vote per voter per poll is a
  unique constraint, not an app-side check two racing requests both pass.
- **Zero-service local run**: without `DATABASE_URL` it uses sqlite, which is
  also what the unit tests migrate and run against.

Tests: `pytest tests/test_polls.py` (unit) ·
`POLL_URL=https://... pytest tests/test_integration.py` ·
`./smoke.sh https://...`.

## Run locally

Use Python 3.10 or newer. From `python/poll-fastapi` inside the examples tree:

```sh
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
pip install pytest httpx
alembic upgrade head
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

With no `DATABASE_URL`, Alembic and the app use `polls.db` in this directory.
Open `http://localhost:8000/docs` for the interactive API. Run
`pytest tests/test_polls.py` in the same environment to test migrations,
validation, successful voting, and duplicate-vote rejection against SQLite.

## Deploy and verify

Follow the [deployment walkthrough](getting-started.md). Deploy this
folder with its `yeeted.yaml`, `alembic.ini`, and `alembic/` revisions. The
manifest requests PostgreSQL; Yeeted supplies `DATABASE_URL` and runs the
Alembic release migration before serving the API on port 8000.

```sh
sh smoke.sh "$PREVIEW_URL"
POLL_URL="$PREVIEW_URL" pytest tests/test_integration.py
```

The smoke script checks health, creates a poll, and reads its initial total of
zero. To verify voting, create a poll with `POST /api/polls` and JSON
`{"question":"What should we build?","options":["Blog","Notes"]}`. Read
`GET /api/polls/{id}` and use an option ID from that response in
`POST /api/polls/{id}/vote` with `{"option_id":1,"voter":"demo-reader"}`,
replacing `1` with the actual option ID. The first vote should return 200;
repeating that voter on the same poll should return 409.

## What to change before real use

The voter is a caller-supplied string, not an authenticated identity. The
unique constraint rejects duplicate voter strings, but does not prevent a
person using multiple strings. Vote totals use a read-modify-write increment,
so concurrent distinct voters can lose increments. Do not use this example
as an election system or a correctness benchmark under load. Add identity
and atomic counter updates before adapting it for concurrent voting.

[All examples](index.md) · [Deploy an example](getting-started.md)
