Skip to content

ayayousef2000/city-stays-booking-backend

Repository files navigation

🏙️ City Stays Booking — Backend API

Django REST API for a city apartment booking platform. Property owners list spaces, guests browse and book. Built with Django 5.x, DRF, dual-layer auth (API Key + JWT), and deployed free on Hugging Face Spaces backed by Neon PostgreSQL and Cloudflare R2.


CI Pipeline Python Django uv Ruff License: MIT


Table of Contents


🏠 What This Project Is

City Stays is a property booking platform — think Airbnb for city apartments. This repository is the backend API only. It is a Django application that:

  • Receives requests from the React frontend over HTTPS
  • Validates authentication (API Key + JWT token)
  • Reads and writes data to PostgreSQL
  • Returns structured JSON responses

The React frontend lives in a separate repository (city-stays-booking). The two services communicate exclusively over HTTP — they share no code.

Why These Tools?

Decision Rationale
uv over pip 10–100× faster installs. One tool replaces pip, venv, and pyenv.
Ruff over flake8 + black + isort One tool, one config block. 100× faster. The 2026 Python standard.
Neon over a VPS database Serverless PostgreSQL — scales to zero, never pay for idle compute.
Hugging Face Spaces over Heroku Completely free Docker hosting. No credit card, ever.
Cloudflare R2 over S3 Zero egress fees. S3-compatible API. Globally fast CDN.
Bruno over Postman Collections are plain files committed to the repo — Git-native.
APPEND_SLASH = False REST URLs are contracts. No silent 301 redirects before DRF runs.

🛠 Tech Stack

Layer Technology Version Purpose
Language Python 3.14 Runtime
Framework Django 5.x Routing, ORM, Admin
REST layer Django REST Framework 3.16.0 JSON API on top of Django
API Key Auth djangorestframework-api-key 3.0.0 Layer 1 — per-client identity
JWT Auth djangorestframework-simplejwt 5.5.0 Layer 2 — per-user identity
API Docs drf-spectacular 0.28.0 Auto-generates Swagger UI + ReDoc
DB Adapter psycopg3 ≥3.2.4 Django → PostgreSQL connector
Package Manager uv 0.7.x+ Installs packages, manages venv
Static Files (local) whitenoise 6.8.2 Serves CSS/JS from Django locally
Static/Media (cloud) Cloudflare R2 S3-compatible, zero egress fees
Config django-environ 0.11.2 Reads .env files safely
Linter + Formatter Ruff 0.9.0+ Replaces flake8 + black + isort
Test Runner pytest-django ≥4.9.0 pytest with Django integration
Coverage pytest-cov ≥6.0.0 Line + branch coverage reporting
DB (local) PostgreSQL 18.3 Windows service on dev machine
DB (cloud) Neon Serverless PostgreSQL Separate branch per environment
Hosting Hugging Face Spaces Free Docker-based hosting
API Testing Bruno latest Git-native request collections

🏗 Architecture

Production Infrastructure

graph TB
    CLIENT["🖥️ React Frontend<br>city-stays-booking.netlify.app"]
    CF_CDN["☁️ Cloudflare CDN<br>Static & Media Files<br>R2 Object Storage"]
    HF["🤗 Hugging Face Spaces<br>Django REST API · Docker<br>city-stays-booking-api.hf.space<br>Port 7860"]
    NEON["🐘 Neon Serverless PostgreSQL<br>Main Branch · Scales to Zero<br>Pooled SSL Connection"]

    CLIENT -->|"HTTPS<br>X-Api-Key + Bearer JWT"| HF
    CLIENT -->|"Static & media files<br>direct from CDN"| CF_CDN
    HF -->|"collectstatic on build<br>boto3 / django-storages"| CF_CDN
    HF -->|"conn_max_age=0 · SSL · psycopg3"| NEON
Loading

Three-Environment Overview

graph LR
    subgraph LOCAL["💻 LOCAL — Windows 10"]
        L1["Django Dev Server<br>127.0.0.1:8000"]
        L2["PostgreSQL 18.3<br>Windows Service · port 5432"]
        L3["Local Filesystem<br>staticfiles/ media/"]
        L4["Swagger UI /api/docs/<br>Open — no auth needed"]
        L1 --- L2
        L1 --- L3
        L1 --- L4
    end

    subgraph STAGING["🔵 STAGING — HF Spaces + Neon + R2"]
        S1["Django App<br>Port 7860 · staging Space"]
        S2["Neon PostgreSQL<br>Staging Branch"]
        S3["Cloudflare R2<br>city-stays-staging bucket"]
        S4["Swagger UI /api/docs/<br>API Key required"]
        S1 --- S2
        S1 --- S3
        S1 --- S4
    end

    subgraph PROD["🔴 PRODUCTION — HF Spaces + Neon + R2"]
        P1["Django App<br>Port 7860 · production Space"]
        P2["Neon PostgreSQL<br>Main Branch"]
        P3["Cloudflare R2<br>city-stays-production bucket"]
        P4["Swagger UI<br>DISABLED — 404"]
        P1 --- P2
        P1 --- P3
        P1 --- P4
    end

    LOCAL -->|"git push develop → CI → deploy"| STAGING
    STAGING -->|"git push main → CI → deploy"| PROD
Loading

How a Request Travels Through the System

flowchart TD
    USER["User clicks Book Now in React app"]
    REACT["React sends POST /api/bookings/<br>Headers: X-Api-Key + Authorization: Bearer token"]
    HF["Hugging Face Spaces<br>receives the HTTPS request"]
    LAYER1{"Layer 1 Check<br>Is X-Api-Key valid?"}
    LAYER2{"Layer 2 Check<br>Is JWT Bearer token valid?"}
    VIEW["Django view runs<br>Reads/writes PostgreSQL<br>Returns JSON response"]
    REACT2["React receives JSON<br>Updates the UI"]
    ERR1["403 Forbidden<br>Wrong or missing API key"]
    ERR2["401 Unauthorized<br>Not logged in or token expired"]

    USER --> REACT
    REACT --> HF
    HF --> LAYER1
    LAYER1 -->|"No"| ERR1
    LAYER1 -->|"Yes"| LAYER2
    LAYER2 -->|"No"| ERR2
    LAYER2 -->|"Yes"| VIEW
    VIEW --> REACT2
Loading

📁 Project Structure

city-stays-booking-backend/
│
├── .github/
│   └── workflows/
│       └── ci.yml                  ← 5-job CI pipeline — runs on every push
│
├── .husky/
│   ├── pre-commit                  ← Ruff lint + format on staged files
│   ├── pre-push                    ← Full pytest suite before every push
│   └── commit-msg                  ← Validates Conventional Commits format
│
├── config/                         ← Django project config (not an app)
│   ├── __init__.py
│   ├── settings.py                 ← All settings — reads from env vars
│   ├── urls.py                     ← Root URL router
│   ├── asgi.py                     ← ASGI entry point
│   └── wsgi.py                     ← WSGI entry point — used by Gunicorn
│
├── core/                           ← First Django app — foundational code
│   ├── migrations/
│   │   └── __init__.py
│   ├── __init__.py
│   ├── admin.py                    ← Model registration for /admin
│   ├── apps.py                     ← App config
│   ├── models.py                   ← DB table definitions
│   ├── views.py                    ← HTTP request handlers
│   ├── urls.py                     ← URL patterns for core app
│   └── tests.py                    ← pytest test suite (uses APIClient)
│
├── bruno/                          ← API test collections (Git-committed)
│   ├── environments/
│   │   ├── local.bru
│   │   ├── staging.bru
│   │   └── production.bru
│   └── health/
│       └── hello-world.bru
│
├── .env                            ← ⚠️ LOCAL ONLY — NEVER commit
├── .gitignore
├── .python-version                 ← Pins Python 3.14 for uv
├── commitlint.config.toml          ← Conventional Commits rules
├── Dockerfile                      ← For Hugging Face Spaces (port 7860)
├── manage.py                       ← Django CLI tool
├── pyproject.toml                  ← All tool config: deps, Ruff, pytest, coverage
├── uv.lock                         ← Exact pinned versions — always commit this
└── start.cmd                       ← Windows shortcut to start dev server

Key Files Explained

config/settings.py — The brain of the application. All configuration reads from environment variables via django-environ. APPEND_SLASH = False prevents CommonMiddleware from issuing 301 redirects that break API clients. JWT lifetimes, DB connection pooling, and security headers are all environment-aware.

config/urls.py — The receptionist. Swagger/ReDoc URLs are only registered when DJANGO_ENV != "production" — the schema is never exposed publicly.

core/tests.py — Uses DRF's APIClient (not Django's Client) so Accept: application/json is sent automatically on every request. This prevents content negotiation from falling back to HTML.

pyproject.toml — Single source of truth for all tooling: dependencies, Ruff rules, pytest config, and coverage thresholds. If a tool behaves differently from what you expect, look here first.

uv.lock — Snapshot of every installed package at its exact version. Always commit this file. CI and Docker both use --frozen to guarantee byte-identical installs.


🌍 Three Environments

Think of environments like a workshop, a dress rehearsal, and live theatre.

Setting Local Staging Production
DJANGO_ENV local staging production
DEBUG True False False
Database PostgreSQL 18.3 on Windows Neon staging branch Neon main branch
Static files Local filesystem Cloudflare R2 Cloudflare R2
Media files Local filesystem Cloudflare R2 Cloudflare R2
Port 8000 7860 7860
SECRET_KEY source .env file HF Space Secret HF Space Secret
conn_max_age 600s 0 ⚠️ 0 ⚠️
Swagger UI ✅ Open 🔑 API Key required ❌ Disabled (404)
JWT Access Token 60 min 15 min 15 min
JWT Refresh Token 7 days 1 day 1 day
HTTPS enforced No Yes Yes

⚠️ Why conn_max_age = 0 on Neon: Neon is serverless — it scales to zero when idle. A persistent connection (conn_max_age > 0) goes stale while Neon sleeps. The next request fails with SSL connection has been closed unexpectedly. Setting it to 0 forces a fresh connection on every request. This is mandatory for all serverless databases. Never change this.


🔐 Security Model

Every API request passes through two sequential security checks.

flowchart LR
    REQ["Incoming Request<br>from React or Bruno"]

    subgraph L1["Layer 1 — API Key — Every Request"]
        AK{"X-Api-Key header<br>present and valid?"}
        AK -->|"No"| R1["403 Forbidden<br>Wrong or missing API key"]
    end

    AK -->|"Yes"| L2

    subgraph L2["Layer 2 — JWT — Protected Endpoints Only"]
        JWT{"Authorization: Bearer<br>token valid and not expired?"}
        JWT -->|"No"| R2["401 Unauthorized<br>Not logged in or token expired"]
    end

    JWT -->|"Yes"| RESP["✅ 200 — Request processed"]

    REQ --> L1
Loading

Layer 1 — API Key (X-Api-Key)

Identifies which client application is making the request. Every single request to every endpoint must include this header. Without it, the server returns 403 Forbidden before any Django logic runs. This blocks bots, scrapers, and unauthorized clients at the door.

How to create an API key:

  1. Go to http://127.0.0.1:8000/admin/ → log in with your superuser credentials
  2. Navigate to API Key Permissions → API Keys → Add API Key
  3. Give it a name (e.g. Local Frontend) and save
  4. Copy the generated key immediately — it cannot be retrieved again

Layer 2 — JWT Bearer Token (Authorization: Bearer ...)

Identifies which user is making the request. A short-lived, cryptographically signed token that proves identity. Required only on protected endpoints (anything that needs to know who is acting).

Header Value Required For
X-Api-Key your-api-key Every endpoint
Authorization Bearer eyJ... Protected endpoints only

Public Endpoints (no auth)

The health-check endpoint (GET /api/hello/) is intentionally public — load balancers and uptime monitors must be able to reach it without credentials. The view uses @permission_classes([AllowAny]) and @authentication_classes([]) to override the global HasAPIKey default.


⚙️ CI Pipeline

Five jobs run on every push to any branch.

flowchart LR
    PUSH["git push"]
    LINT["Job 1: Fast Linting<br>ruff check + ruff format --check<br>Fails in under 10s"]
    TYPE["Job 2: Type Checking<br>mypy placeholder<br>Stable slot in pipeline graph"]
    TEST["Job 3: Integration Tests<br>pytest against real PostgreSQL 16<br>Coverage report uploaded"]
    SEC["Job 4: Security & Audit<br>pip-audit scans uv.lock for CVEs"]
    GATE["Job 5: CI Passed ✅<br>Aggregator — branch protection<br>targets this job only"]

    PUSH --> LINT
    LINT --> TYPE
    LINT --> TEST
    LINT --> SEC
    TYPE & TEST & SEC --> GATE
Loading

Why Each Job Exists

Job 1 — Fast Linting: Catches undefined variables, unused imports, formatting drift, and security anti-patterns. Takes under 10 seconds. Developers get instant feedback on every push.

Job 2 — Type Checking: Placeholder for mypy/pyright. Kept in the graph so adding type checking later never requires touching branch protection rules.

Job 3 — Integration Tests: Runs against a real PostgreSQL 16 service container — not SQLite. Catches database-specific behaviour (JSON fields, SELECT FOR UPDATE, index usage). Produces coverage.xml uploaded as an artifact.

Job 4 — Security & Audit: pip-audit scans the exact locked versions from uv.lock against the CVE database. Uses --no-hashes export to avoid false-positive hash mismatches from binary wheel build dependencies.

Job 5 — CI Gate: A single aggregator job that is either green or red. GitHub branch protection targets only this job name, so adding or removing jobs 1–4 never requires touching branch protection settings.

Node.js Compatibility

All actions (actions/checkout@v4, astral-sh/setup-uv@v6, actions/upload-artifact@v4) target the Node.js 24 runtime, ahead of GitHub's June 2026 enforcement deadline.


🌿 Git Branching Strategy

gitGraph
    commit id: "ci: setup project tooling"
    branch develop
    checkout develop
    commit id: "feat: hello world ✅"
    branch feature/auth
    checkout feature/auth
    commit id: "feat(auth): api key middleware"
    commit id: "feat(auth): jwt login + refresh"
    commit id: "test(auth): full auth test suite"
    checkout develop
    merge feature/auth id: "merge: full auth layer"
    branch feature/properties
    checkout feature/properties
    commit id: "feat(properties): property model"
    commit id: "feat(properties): list + detail API"
    checkout develop
    merge feature/properties
    checkout main
    merge develop id: "release: auth + properties"
Loading

Branch Rules

Branch Purpose Access
feature/* One feature in development Direct push
develop Integration — always deployable to staging PR only, CI must pass
main Production — always live PR from develop only, CI must pass

Daily Development Flow

flowchart LR
    A["Create feature branch<br>from develop"] --> B["Write code locally"]
    B --> C["Local checks pass<br>ruff + pytest + django check"]
    C --> D["Push to GitHub<br>CI runs automatically"]
    D --> E{"CI green?"}
    E -->|"No"| B
    E -->|"Yes"| F["Open PR to develop<br>Merge when ready"]
    F --> G["Auto-deploy to staging<br>HF Spaces"]
    G --> H["Test on staging<br>with Bruno"]
    H --> I{"Staging verified?"}
    I -->|"No"| B
    I -->|"Yes"| J["Open PR from develop to main"]
    J --> K["Auto-deploy to production"]
Loading

Conventional Commits

All commit messages follow the Conventional Commits spec. Husky enforces this on every git commit.

feat(auth): add JWT login endpoint
fix(bookings): prevent double-booking race condition
test(properties): add coverage for filter logic
ci: upgrade actions to Node.js 24
docs: update API auth section in README
refactor(core): extract booking logic to service layer

💻 Local Development Setup

Prerequisites

Install these tools on Windows 10 before starting:

Tool Download Verify
Git git-scm.com git --version
uv PowerShell (see below) uv --version
PostgreSQL 18.3 enterprisedb.com psql --version
VS Code code.visualstudio.com Open app
Node.js LTS nodejs.org node --version

Install uv (PowerShell as Administrator):

powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

Configure Git (prevents Ruff formatting failures from CRLF line endings):

git config --global user.name "Your Full Name"
git config --global user.email "your-email@example.com"
git config --global core.autocrlf false

Add PostgreSQL to PATH (CMD as Administrator, then open a new window):

setx PATH "%PATH%;C:\Program Files\PostgreSQL\18\bin" /M

Recommended VS Code Extensions

Extension Publisher Purpose
Python Microsoft Language support
Pylance Microsoft Type checking
Ruff Astral Software Lint + format on save
Django Baptiste Darthenay Template + URL support
GitLens GitKraken Inline Git history
PostgreSQL Chris Kolkman Browse DB from VS Code
Docker Microsoft Container management

Step-by-Step: Phase 1 — Hello World Locally

flowchart TD
    A(["▶ Start"]) --> B["Clone repo and create develop branch"]
    B --> C["Create PostgreSQL database and user"]
    C --> D["Bootstrap Django project with uv"]
    D --> E["Write all config files"]
    E --> F["Create .env file with local secrets"]
    F --> G["Run migrations and create superuser"]
    G --> H["Run all local checks"]
    H --> I{"All green?"}
    I -->|"No — fix the error"| H
    I -->|"Yes"| J["First commit and push to GitHub"]
    J --> K{"CI passes?"}
    K -->|"No — fix the error"| H
    K -->|"Yes"| L(["✅ Phase 1 Complete"])
Loading

1. Clone and Branch

cd C:\Users\YOUR_USERNAME\Documents\GitHub
git clone https://github.com/YOUR_USERNAME/city-stays-booking-backend.git
cd city-stays-booking-backend
git checkout -b develop
git push -u origin develop

2. Create the Local Database

psql -U postgres -h 127.0.0.1
CREATE USER city_stays_booking_user WITH PASSWORD 'CityStays#2026';
CREATE DATABASE city_stays_booking_db OWNER city_stays_booking_user;
GRANT ALL PRIVILEGES ON DATABASE city_stays_booking_db TO city_stays_booking_user;
ALTER USER city_stays_booking_user CREATEDB;
\q

3. Bootstrap with uv

uv python pin 3.14
uv add django djangorestframework djangorestframework-api-key
uv add djangorestframework-simplejwt drf-spectacular
uv add "psycopg[binary]>=3.2.4,<3.3.0" django-environ whitenoise gunicorn
uv add --dev ruff pytest-django "pytest-cov>=6.0.0"
uv run django-admin startproject config .
uv run python manage.py startapp core

4. Create the .env File

⚠️ Create this file in VS Code only. Bottom-right corner must show UTF-8 and LF — never CRLF. Never use Notepad.

# .env — Local Windows 10 development — NEVER commit this file

DJANGO_ENV=local
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost
DATABASE_URL=postgres://city_stays_booking_user:CityStays#2026@127.0.0.1:5432/city_stays_booking_db
SECRET_KEY=replace-this-with-output-of-the-command-below

Generate a real SECRET_KEY:

uv run python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"

5. Migrate and Verify

uv run python manage.py migrate
uv run python manage.py createsuperuser
uv run python manage.py runserver
URL Expected Result
http://127.0.0.1:8000/api/hello/ {"status": "ok", "message": "City Stays Booking API is running 🏙️", ...}
http://127.0.0.1:8000/api/docs/ Swagger UI — full interactive schema
http://127.0.0.1:8000/api/redoc/ ReDoc documentation
http://127.0.0.1:8000/admin/ Django admin login

6. Run All Checks Before Committing

uv run ruff check .                      # Lint
uv run ruff format . --check             # Format
uv run python manage.py check            # Django system check
uv run pytest                            # Full test suite

🔵 Staging Deployment (Phase 2)

flowchart TD
    A(["✅ Phase 1 Complete"]) --> B["Create Neon project + staging branch"]
    B --> C["Create Cloudflare account + R2 bucket"]
    C --> D["Add django-storages + boto3"]
    D --> E["Create Hugging Face account + staging Space"]
    E --> F["Write Dockerfile"]
    F --> G["Set all Secrets in HF Space settings"]
    G --> H["Run Neon migrations from local machine"]
    H --> I["Push to HF Space manually — first deploy"]
    I --> J["Verify staging with Bruno"]
    J --> K["Add HF_TOKEN to GitHub Secrets for CI/CD"]
    K --> L(["✅ Staging Live"])
Loading

Neon Database Setup

  1. Sign up at neon.tech (free, no credit card)
  2. Create project: city-stays-booking — choose region closest to your users
  3. Neon creates a main branch automatically (this will be production)
  4. Go to Branches → New Branch — name: staging
  5. From the staging branch, copy both connection strings:
# Pooled URL — used by the running app
postgresql://user:pass@ep-XXXX-pooler.eu-west-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require

# Direct URL — used only for running migrations
postgresql://user:pass@ep-XXXX.eu-west-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require

⚠️ Always use the Direct URL for migrate and the Pooled URL for the app. Using pooled for migrations causes incomplete migrations. Using direct in the app causes SSL errors under load.

Cloudflare R2 Setup

  1. Sign up at cloudflare.com (free)
  2. Go to R2 Object Storage → Create bucket
    • city-stays-staging
    • city-stays-production
  3. R2 → Manage R2 API tokens → Create API token
    • Permissions: Object Read & Write
    • Scope: Both buckets
  4. Copy Access Key ID and Secret Access Key immediately
  5. For each bucket: Settings → Public access → Allow public access

Hugging Face Space Setup

  1. Sign up at huggingface.co (free, no credit card)
  2. Spaces → New Space
    • Name: city-stays-booking-staging
    • SDK: Docker ← critical
    • Hardware: CPU basic (free)
    • Visibility: Private
  3. Go to Space Settings → Secrets and add:
Secret Name Value
DJANGO_ENV staging
DEBUG False
SECRET_KEY your generated secret key
ALLOWED_HOSTS YOUR_HF_USERNAME-city-stays-booking-staging.hf.space
DATABASE_URL Neon pooled URL for staging branch
DATABASE_URL_DIRECT Neon direct URL for staging branch
CLOUDFLARE_R2_BUCKET city-stays-staging
CLOUDFLARE_R2_ENDPOINT https://ACCOUNT_ID.r2.cloudflarestorage.com
CLOUDFLARE_R2_ACCESS_KEY your R2 access key
CLOUDFLARE_R2_SECRET_KEY your R2 secret key
CLOUDFLARE_R2_PUBLIC_URL https://pub-XXXX.r2.dev

Dockerfile

# Dockerfile — Hugging Face Spaces requires port 7860
FROM python:3.14-slim

WORKDIR /app

RUN apt-get update && apt-get install -y \
    libpq-dev \
    gcc \
    && rm -rf /var/lib/apt/lists/*

COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv

COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev

COPY . .

RUN uv run python manage.py collectstatic --noinput --clear

EXPOSE 7860

CMD ["uv", "run", "gunicorn", "config.wsgi:application", \
     "--bind", "0.0.0.0:7860", \
     "--workers", "2", \
     "--timeout", "120"]

🔴 Production Deployment (Phase 3)

Identical process to staging, with two differences:

  1. Create a second HF Space: city-stays-booking-api
  2. Set DATABASE_URL to the Neon main branch connection strings

⚠️ Never run migrations against the production Neon main branch from your local machine during active traffic. Always schedule migrations during a maintenance window or use Neon's branching to test the migration on a copy first.


📖 API Documentation

flowchart TD
    REQ["Request to /api/docs/"]
    ENV{"Which environment?"}
    LOCAL["✅ Open access<br>No auth required<br>Full interactive schema"]
    STAGING["🔑 API Key required<br>Add X-Api-Key in Swagger UI<br>Authorize dialog"]
    PROD["❌ 404 Not Found<br>Docs disabled in production<br>Schema never exposed publicly"]

    REQ --> ENV
    ENV -->|"local"| LOCAL
    ENV -->|"staging"| STAGING
    ENV -->|"production"| PROD
Loading
URL Local Staging Production
/api/docs/ ✅ Open 🔑 API Key ❌ 404
/api/redoc/ ✅ Open 🔑 API Key ❌ 404
/api/schema/ ✅ Open 🔑 API Key ❌ 404

Docs are disabled in production because exposing the full API schema gives attackers a detailed roadmap of every endpoint, expected payload, and authentication mechanism.


🧪 Testing with Bruno

Bruno stores API test collections as plain text files committed to this repository. Your whole team shares the same test collections automatically.

Collection structure:

bruno/
├── environments/
│   ├── local.bru       ← http://127.0.0.1:8000
│   ├── staging.bru     ← https://YOUR_HF_USERNAME-city-stays-booking-staging.hf.space
│   └── production.bru  ← https://YOUR_HF_USERNAME-city-stays-booking-api.hf.space
└── health/
    └── hello-world.bru

bruno/health/hello-world.bru:

meta {
  name: Hello World
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/hello/
  body: none
  auth: none
}

Download Bruno at usebruno.com. Open the bruno/ folder inside Bruno and select the environment matching your target.


🗺 Feature Build Order

flowchart LR
    F1["1. Hello World<br>✅ Done"] --> F2["2. DRF + Swagger<br>Bruno setup"]
    F2 --> F3["3. API Key Auth<br>all environments"]
    F3 --> F4["4. JWT Auth<br>login · refresh · logout · blacklist"]
    F4 --> F5["5. Custom User Model<br>email-based login"]
    F5 --> F6["6. Domain Models<br>Property · Room · Booking · Guest"]
    F6 --> F7["7. Property API<br>CRUD + filtering"]
    F7 --> F8["8. Booking API<br>transaction.atomic()<br>availability logic"]
    F8 --> F9["9. HF Deploy<br>Dockerfile + Cloudflare R2<br>staging verified"]
Loading

Feature Development Workflow

# 1. Create feature branch from develop
git checkout develop && git pull origin develop
git checkout -b feature/your-feature-name

# 2. Write code

# 3. Run all checks before committing
uv run ruff check .
uv run ruff format . --check
uv run python manage.py check
uv run pytest

# 4. Commit — Husky runs ruff automatically on staged files
git add .
git commit -m "feat(scope): description of what you built"

# 5. Push and let CI run
git push origin feature/your-feature-name

# 6. Open PR to develop → merge when CI is green
# 7. Test on staging with Bruno
# 8. Merge develop to main when staging is verified

⚡ Daily Commands

# ── Navigation ─────────────────────────────────────────────────────────────
cd C:\Users\YOUR_USERNAME\Documents\GitHub\city-stays-booking-backend

# ── Server ─────────────────────────────────────────────────────────────────
uv run python manage.py runserver

# ── Testing ────────────────────────────────────────────────────────────────
uv run pytest
uv run pytest --cov=. --cov-report=term-missing
uv run pytest -v -k "test_hello"          # Run a specific test

# ── Linting & Formatting ───────────────────────────────────────────────────
uv run ruff check .                        # Lint check
uv run ruff check --fix .                  # Auto-fix lint issues
uv run ruff format . --check               # Format check
uv run ruff format .                       # Auto-format all files

# ── Django Management ──────────────────────────────────────────────────────
uv run python manage.py check              # Django system check (local)
uv run python manage.py check --deploy     # Simulate production settings check
uv run python manage.py makemigrations
uv run python manage.py migrate
uv run python manage.py createsuperuser
uv run python manage.py shell
uv run python manage.py flushexpiredtokens # Flush expired JWT blacklist tokens

# ── Neon Staging Migration ─────────────────────────────────────────────────
set DATABASE_URL=postgresql://user:pass@ep-XXXX.eu-west-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require
uv run python manage.py migrate

# ── Packages ───────────────────────────────────────────────────────────────
uv add package-name
uv add --dev package-name

# ── Docker ─────────────────────────────────────────────────────────────────
docker build -t city-stays-api .
docker run -p 7860:7860 --env-file .env city-stays-api

# ── Manual HF Deploy ───────────────────────────────────────────────────────
git push hf-staging develop:main
git push hf-prod main:main

⚠️ Known Pitfalls

Problem Cause Fix
.env causes UnicodeDecodeError File saved as UTF-16 or CRLF Create .env only in VS Code — verify UTF-8 + LF in the bottom-right corner
psql not recognized in CMD PostgreSQL bin not in PATH Run setx PATH as Administrator, then open a new CMD window
Connection refused on port 5432 PostgreSQL service stopped net start postgresql-x64-18 as Administrator
Ruff format check fails in CI Files not formatted locally Run uv run ruff format . then commit the reformatted files
Django admin login fails Wrong password Admin uses the createsuperuser password, not the PostgreSQL password
HF Space shows blank page Wrong port in Dockerfile Port must be 7860 — no exceptions
HF Space build fails Missing system deps for psycopg3 Dockerfile must have libpq-dev gcc in the apt-get install line
Neon SSL error Missing SSL params in URL All Neon URLs need ?sslmode=require&channel_binding=require
SSL connection closed unexpectedly on Neon conn_max_age > 0 set Set CONN_MAX_AGE = 0 for all non-local environments — mandatory
ProgrammingError: relation does not exist Migrations not run CI runs manage.py migrate before pytest — check your CI logs
Neon migration hangs Using pooled URL for migrate Always use DATABASE_URL_DIRECT (no -pooler) for migrations
HF Space secrets not loading Added as Variables not Secrets In HF Space Settings, move sensitive values from Variables to Secrets
collectstatic fails in Docker build SECRET_KEY not set at build time Ensure all required env vars are set as HF Space Secrets
Swagger shows 404 in production Intentionally disabled Use Bruno or curl for production testing
Swagger shows 401 in staging Missing API key Add X-Api-Key in the Swagger UI Authorize dialog
API key cannot be retrieved Not copied at creation Copy immediately after creation — it cannot be seen again. Create a new key.
HF_TOKEN CI deploy fails Token has read-only scope Regenerate token with Write scope on Hugging Face
HF cold start slow (~10s) Free tier spins down after idle Expected on free tier. Upgrade hardware tier to prevent spin-down.
Cloudflare R2 returns 403 Bucket not set to public access R2 bucket settings → Public access → Allow public access
Static files not found after deploy collectstatic not run Ensure RUN uv run python manage.py collectstatic --noinput --clear is in Dockerfile
uv.lock not committed Developer forgot Always commit uv.lock — CI and Docker both depend on it
T201 print found in CI Bare print() in production code Use sys.stdout.write() or remove the print statement
Tests return 301 instead of 200 Using django.test.Client Use rest_framework.test.APIClient — it sends Accept: application/json
Tests return text/html content-type Missing Accept header APIClient sets this automatically; Django's Client does not
pip-audit hash mismatch uv export --require-hashes used Use uv export --no-hashes — pip-audit still validates exact CVE versions

📐 Naming Conventions

Context Name Rule
Git repository city-stays-booking-backend Hyphens — GitHub standard
Python/Django internals city_stays_booking_api Underscores — Python module rule
Django config module config/ Separate from app modules
PostgreSQL DB (local) city_stays_booking_db Underscores — SQL standard
PostgreSQL user (local) city_stays_booking_user Underscores — SQL standard
Neon project city-stays-booking Hyphens — Neon convention
HF Space (staging) city-stays-booking-staging Hyphens — HF convention
HF Space (production) city-stays-booking-api Hyphens — HF convention
Frontend repo city-stays-booking Existing — kept separate
Commit messages feat(scope): description Conventional Commits
Feature branches feature/short-description Lowercase hyphens

📊 Current Status

Phase 1 — ✅ Complete

  • GET /api/hello/ returns 200 OK with JSON — verified locally with PostgreSQL
  • Full pytest suite green — 8 tests, all using APIClient, 89% coverage
  • Ruff lint + format both clean
  • CI pipeline fully configured — 5 jobs, all green
  • Branch protection on main requiring the CI gate job to pass
  • Husky hooks enforcing: Ruff on pre-commit, pytest on pre-push, Conventional Commits on commit-msg

Phase 2 — 🔵 Next

  1. Create Neon project → staging branch → copy both connection strings
  2. Create Cloudflare account → two R2 buckets (staging + production)
  3. Add django-storages[s3] + boto3 → update settings.py STORAGES block
  4. Create Hugging Face account → staging Space (Docker, free tier)
  5. Write Dockerfile → set all HF Space Secrets → first manual deploy
  6. Verify staging with Bruno → run smoke test collection
  7. Add HF_TOKEN to GitHub Secrets → enable CI auto-deploy
  8. Create production Space → repeat deployment for production

City Stays Booking Backend

Django 5.x · Python 3.14 · uv · Ruff · PostgreSQL
Local: PostgreSQL 18.3 · Cloud: Neon + Hugging Face Spaces + Cloudflare R2
Security: API Key (Layer 1) + JWT with rotation and blacklist (Layer 2)
Testing: Bruno (functional) · pytest (automated) · pip-audit (security)

About

Django REST API for City Stays — a property booking platform. Django 6, DRF, JWT auth, Neon PostgreSQL, Cloudflare R2, deployed on Hugging Face Spaces. Python 3.14 · uv · Ruff.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors