shortpy is a small, self-hosted URL shortener built with FastAPI and SQLite. It gives you durable short links, HTTP 301 redirects, a clean web UI, OpenAPI docs, rate limiting, Docker support, and click analytics without needing an external service.
- FastAPI application with automatic OpenAPI docs at
/docs - SQLite storage with SQLAlchemy models and Alembic migrations
- Random or custom short codes
- HTTP 301 redirects
- Click analytics with timestamp, IP address, user agent, and referer
- Simple responsive web UI
- API-first design for automation
- Rate limiting for link creation
- Reserved route protection so generated links do not collide with
/docs,/api, or static assets - Docker and Docker Compose deployment
- pytest + httpx/FastAPI TestClient coverage
- GitHub Actions CI for linting and tests
python -m venv .venv
source .venv/bin/activate
python -m pip install -e ".[dev]"
cp .env.example .env
alembic upgrade head
uvicorn shortpy.main:app --reloadOpen http://localhost:8000 for the web UI or http://localhost:8000/docs for the API docs.
docker compose up --buildThe Compose setup stores SQLite data in a named Docker volume and exposes the app at http://localhost:8000.
Create a short link:
curl -X POST http://localhost:8000/api/links \
-H "Content-Type: application/json" \
-d '{"target_url":"https://example.com/a/very/long/path"}'Create a custom short code:
curl -X POST http://localhost:8000/api/links \
-H "Content-Type: application/json" \
-d '{"target_url":"https://example.com/docs","custom_code":"launch"}'Read analytics:
curl http://localhost:8000/api/links/launchRedirect:
curl -I http://localhost:8000/launch| Variable | Default | Description |
|---|---|---|
SHORTPY_BASE_URL |
http://localhost:8000 |
Public base URL used when returning short links. |
SHORTPY_DATABASE_URL |
sqlite:///./shortpy.db |
SQLAlchemy database URL. |
SHORTPY_ENV |
development |
Environment label for deployments. |
SHORTPY_RATE_LIMIT |
30/minute |
SlowAPI rate limit applied to link creation. |
SHORTPY_CODE_LENGTH |
7 |
Generated short code length, from 4 to 16 characters. |
Run migrations locally:
alembic upgrade headCreate a new migration after model changes:
alembic revision --autogenerate -m "describe change"The app also creates missing tables at startup for lightweight self-hosted deployments. Alembic remains the recommended path for controlled production upgrades.
python -m pip install -e ".[dev]"
ruff check .
alembic upgrade head
pytest --cov=shortpy --cov-report=term-missingBefore opening a pull request, run linting and tests locally and include any behavior changes in the README or tests.
CI and Docker builds use constraints.txt to keep dependency resolution reproducible; regenerate it after intentional dependency upgrades.
src/shortpy/
config.py Settings from environment variables
database.py SQLAlchemy engine and session wiring
main.py FastAPI routes, UI, rate limiting, redirects
models.py SQLAlchemy models
schemas.py Pydantic API schemas
services.py Link creation, lookup, and analytics logic
- Put the app behind HTTPS before exposing it publicly.
- Set
SHORTPY_BASE_URLto the externally reachable origin. - Persist the SQLite database file with a host mount or Docker volume.
- Consider a reverse proxy such as Caddy, Traefik, or nginx for TLS and access logs.
- For high-write workloads, move from SQLite to PostgreSQL using the same SQLAlchemy model layer.
Contributions are welcome. See CONTRIBUTING.md for local setup, testing, and pull request expectations. Changes are tracked in CHANGELOG.md, and dependency updates are monitored through Dependabot.
MIT
