TiwariDivya25/DevBlog

A full-stack blogging platform built with Django REST Framework (backend) and React + Vite (frontend). Users can sign up, log in, create posts, and read posts from other users.

★ 0Forks 0JavaScriptGitHub ↗Compare

Project website ↗

README

DevBlog

A full-stack blogging platform built with Django REST Framework (backend) and React + Vite (frontend). Users can sign up, log in, create posts, and read posts from other users.

Screenshot 2026-05-25 170857 Screenshot 2026-05-25 171912 Screenshot 2026-05-25 171456

Table of Contents

  1. Features
  2. Tech Stack
  3. Architecture Overview
  4. Project Structure
  5. Authentication and Security Model
  6. Data Model
  7. API Reference
  8. Request and Response Flows
  9. Environment Variables
  10. Local Development
  11. Deployment
  12. Known Limitations and Future Work

Features

  • User accounts. Sign up and log in with a username and password. Sessions are server side, tracked with a secure HTTP only cookie.
  • Create posts. Authenticated users can write a post with a title and body.
  • Read posts. Anyone, logged in or not, can browse the feed and open a post to read the full text.
  • Full post view. Clicking a post title opens a dedicated detail page instead of a truncated preview.
  • Edit posts. Authors can edit their own posts. The same editor UI is reused for both creating and editing, switching modes based on whether an existing post is loaded.
  • Delete posts. Authors can delete their own posts, from the feed or from the detail page.
  • Ownership enforcement. Editing or deleting a post you do not own is rejected by the backend itself, not just hidden in the UI.
  • Responsive, styled UI. No component library. All styling is inline, custom built.

Tech Stack

Layer Technology
Backend framework Django 6, Django REST Framework
Backend auth Django sessions + CSRF tokens
Database PostgreSQL in production, SQLite for local dev
Static files WhiteNoise
WSGI server Gunicorn
Frontend framework React 19
Build tool Vite 8
Styling Inline CSS in JS, no UI library
Backend hosting Render (free tier)
Frontend hosting Vercel

Architecture Overview

The app is two separately deployed services that communicate entirely over HTTP. There is no server side rendering and no shared codebase between frontend and backend beyond the JSON contract of the API.

flowchart LR
    subgraph Browser
        UI[React SPA<br/>Vite build]
    end
 
    subgraph Vercel
        Static[Static hosting<br/>serves the built React app]
    end
 
    subgraph Render
        Django[Django + DRF<br/>gunicorn]
        DB[(PostgreSQL)]
    end
 
    UI -- "loads index.html + JS bundle" --> Static
    UI -- "fetch() with credentials: include / JSON over HTTPS" --> Django
    Django -- "ORM queries" --> DB
    Django -- "Set-Cookie: sessionid, csrftoken" --> UI
Loading

Key architectural decisions:

  • Cross origin by design. The frontend domain (*.vercel.app) and backend domain (*.onrender.com) are different origins. This forces explicit CORS and CSRF configuration rather than letting the browser's same origin defaults paper over the problem.
  • Session auth, not JWT. Login sets a sessionid cookie. The browser sends it automatically on every request via credentials: 'include'. This avoids the complexity of token refresh and storage, at the cost of needing correct cross site cookie settings (SameSite=None; Secure) in production.
  • CSRF token delivered in the JSON body, not read from the cookie. This is the single most important non-obvious design decision in the project, explained in detail below.
  • Environment driven settings. The same settings.py runs against SQLite locally and PostgreSQL in production, switching automatically based on the DATABASE_URL environment variable, with no code change required.

Project Structure

DevBlog/
├── backend/
│   ├── backend/
│   │   ├── settings.py       # environment-driven config (dev + prod)
│   │   ├── urls.py           # root URL routing, registers the API router
│   │   └── wsgi.py
│   ├── posts/
│   │   ├── models.py         # Post model
│   │   ├── serializers.py    # PostSerializer
│   │   ├── views.py          # PostViewSet + auth views (signup/login/logout/me)
│   │   └── urls.py           # legacy, unused; real routing lives in backend/urls.py
│   ├── requirements.txt
│   ├── Procfile               # gunicorn start command reference
│   └── runtime.txt
└── frontend/
    ├── src/
    │   ├── App.jsx            # top-level state, routing between views, layout
    │   ├── lib/
    │   │   ├── api.js         # fetch wrapper, CSRF token storage
    │   │   └── format.js      # date/read-time/initials helpers
    │   └── components/
    │       ├── Avatar.jsx
    │       ├── Toast.jsx
    │       ├── FeaturedCard.jsx
    │       ├── PostCard.jsx
    │       ├── AuthForm.jsx
    │       ├── Editor.jsx     # shared create/edit post form
    │       ├── PostDetail.jsx
    │       └── Sidebar.jsx
    ├── index.html
    └── vite.config.js

Authentication and Security Model

Why session auth here, and what it costs

Django's built in session framework is the simplest way to authenticate a small app: log in once, the server remembers you via a signed cookie, every subsequent request is automatically authenticated by the browser attaching that cookie. The cost is that cookies behave differently across origins, and the app deliberately deploys frontend and backend on different origins, so this had to be solved properly rather than avoided.

CSRF: the actual problem and the actual fix

Django protects state changing requests (POST, PATCH, DELETE) with a CSRF token. The standard pattern is: the server sets a csrftoken cookie, client side JavaScript reads that cookie with document.cookie, and sends its value back in an X-CSRFToken header.

That pattern silently breaks the moment frontend and backend are on different domains. A cookie set by a response from onrender.com is stored under the onrender.com domain. JavaScript running on a page served from vercel.app can never read a cookie that belongs to a different domain, regardless of SameSite or Secure settings. The result was getCsrf() always returning undefined, which gets serialized into the request header as the literal string "undefined", which Django then rejects with a "CSRF token has incorrect length" error, because nine characters is not a valid token.

The fix implemented in this project: the backend's /api/auth/me/ and /api/auth/login/ endpoints return the CSRF token directly in the JSON response body (via Django's get_token(request)), not just as a cookie. The frontend stores that value in an in-memory JavaScript variable (src/lib/api.js) and attaches it manually to the X-CSRFToken header on every mutating request. The cookie is still set (Django requires it to exist server side to validate against), but the frontend never depends on reading it.

sequenceDiagram
    participant Browser
    participant Django as Django backend
 
    Browser->>Django: GET /api/auth/me/
    Django-->>Browser: Set-Cookie: csrftoken=...
    Django-->>Browser: Body: { username, csrfToken }
    Note over Browser: csrfToken stored in memory,<br/>not read from document.cookie
 
    Browser->>Django: POST /api/posts/ (Header: X-CSRFToken from memory)
    Django->>Django: Compares header token against session's expected token
    Django-->>Browser: 201 Created
Loading

Ownership enforcement

Early in development, the PostViewSet only checked IsAuthenticatedOrReadOnly, meaning any logged in user could technically send a DELETE or PATCH to another user's post ID directly against the API, even though the UI only showed those buttons to the post's actual author. A custom IsAuthorOrReadOnly permission class was added, checked at the object level, so the backend itself rejects the request with 403 Forbidden if request.user does not match post.author. The UI hiding the buttons is a convenience, not the security boundary.

CORS and cookie flags in production

CORS_ALLOWED_ORIGINS = ["https://<your-vercel-app>.vercel.app"]
CORS_ALLOW_CREDENTIALS = True
SESSION_COOKIE_SAMESITE = "None"
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = "None"
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = ["https://<your-vercel-app>.vercel.app"]

SameSite=None is required for the browser to send the session cookie on a cross site request at all. Secure=True is required alongside it, since SameSite=None cookies are rejected by browsers unless served over HTTPS. Locally, where frontend and backend share localhost, these are relaxed back to SameSite=Lax and Secure=False so cookies work over plain HTTP during development.


Data Model

erDiagram
    USER ||--o{ POST : writes
    USER {
        int id
        string username
        string password_hash
    }
    POST {
        int id
        string title
        text content
        int author_id FK
        datetime created
    }
Loading

Post.author is a foreign key to Django's built in User model. PostSerializer exposes author_name (the username) as a read only derived field, so the frontend never has to look up a user by ID separately.


API Reference

Method Endpoint Auth required Description
GET /api/posts/ No List all posts
POST /api/posts/ Yes Create a post, author set from session
GET /api/posts/:id/ No Get a single post
PATCH /api/posts/:id/ Yes, owner only Update a post
DELETE /api/posts/:id/ Yes, owner only Delete a post
POST /api/auth/signup/ No Register a new user
POST /api/auth/login/ No Log in, returns csrfToken
POST /api/auth/logout/ Yes Log out, clears session
GET /api/auth/me/ No Get current user if any, and csrfToken

All requests are sent with credentials: 'include' from the frontend so the session cookie is always attached once it exists.


Request and Response Flows

1. App load

sequenceDiagram
    participant React
    participant Django
 
    React->>Django: GET /api/auth/me/
    React->>Django: GET /api/posts/
    Django-->>React: { username, csrfToken } or 401
    Django-->>React: list of posts
    React->>React: store csrfToken in memory, render feed
Loading

Both requests fire in parallel on mount using Promise.all. If /auth/me/ returns 401, the user is simply treated as logged out; the posts still load and render, since reading the feed does not require authentication.

2. Sign up

sequenceDiagram
    participant React
    participant Django
    participant DB
 
    React->>Django: POST /api/auth/signup/ with username, password
    Django->>DB: check username uniqueness
    Django->>DB: create_user(), hashes password
    Django-->>React: 201 Created
Loading

Signup does not automatically log the user in; the frontend's handleAuth function is called for whichever endpoint, signup or login, matches the current view, and only the login response carries a session cookie and CSRF token.

3. Log in

sequenceDiagram
    participant React
    participant Django
 
    React->>Django: POST /api/auth/login/ with username, password
    Django->>Django: authenticate() checks credentials
    Django->>Django: login() creates session, sets sessionid cookie
    Django-->>React: 200, { username, csrfToken }
    React->>React: store csrfToken, set user, redirect to feed
Loading

4. Create a post

sequenceDiagram
    participant React
    participant Django
    participant DB
 
    React->>Django: POST /api/posts/ with title, content (Header: X-CSRFToken, Cookie: sessionid)
    Django->>Django: verify CSRF token against session
    Django->>Django: verify IsAuthenticatedOrReadOnly
    Django->>DB: insert post, author = request.user
    DB-->>Django: new row
    Django-->>React: 201, created post
    React->>React: prepend post to feed, show toast, redirect to list
Loading

5. Open a full post

sequenceDiagram
    participant React
 
    React->>React: user clicks post title
    React->>React: set selected post, switch view to detail
Loading

No network request is made here. The post object is already in the frontend's posts state from the initial feed load, so opening the detail view is a pure client side state change, not a fetch.

6. Edit a post

sequenceDiagram
    participant React
    participant Django
    participant DB
 
    React->>React: user clicks Edit, editor pre-filled with title and content
    React->>Django: PATCH /api/posts/:id/ with title, content (Header: X-CSRFToken)
    Django->>Django: IsAuthorOrReadOnly checks obj.author == request.user
    Django->>DB: update post
    Django-->>React: 200, updated post
    React->>React: replace post in local state, return to detail view
Loading

If the requester is not the post's author, the permission check rejects with 403 Forbidden before any database write happens, regardless of what the frontend UI would have allowed.

7. Delete a post

sequenceDiagram
    participant React
    participant Django
    participant DB
 
    React->>Django: DELETE /api/posts/:id/ (Header: X-CSRFToken)
    Django->>Django: IsAuthorOrReadOnly check
    Django->>DB: delete post row
    Django-->>React: 204 No Content
    React->>React: remove post from local state, return to list if it was open
Loading

8. Log out

sequenceDiagram
    participant React
    participant Django
 
    React->>Django: POST /api/auth/logout/ (Header: X-CSRFToken)
    Django->>Django: logout() clears session
    Django-->>React: 200 OK
    React->>React: clear user, return to feed
Loading

Environment Variables

Backend, backend/.env locally, dashboard variables in production

Variable Local default Production value
SECRET_KEY falls back to a hardcoded dev key long random string, unique per environment
DEBUG True False
ALLOWED_HOSTS not required your Render domain, e.g. devblog-backend.onrender.com
DATABASE_URL not required, falls back to SQLite Postgres connection string
CORS_ALLOWED_ORIGINS http://localhost:5173 your Vercel URL
CSRF_TRUSTED_ORIGINS http://localhost:5173 your Vercel URL

Frontend, frontend/.env locally, dashboard variable in production

Variable Local value Production value
VITE_API_URL http://localhost:8000/api https://your-render-domain/api

Local Development

Backend

cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python manage.py migrate
python manage.py runserver

Runs on http://localhost:8000

Frontend

cd frontend
npm install
npm run dev

Runs on http://localhost:5173


Deployment

  • Backend on Render. Deployed from the backend/ root directory. Build command installs dependencies and runs collectstatic. Start command runs migrations then starts gunicorn, since the free tier has no shell access to run one-off management commands manually.
  • Frontend on Vercel. Deployed from the frontend/ root directory, auto-detected as a Vite project.
  • Database. PostgreSQL, either Render's own free instance, which expires after 30 days on the free tier, or an external provider such as Neon.
  • Both platforms auto-redeploy on every push to main.

Known Limitations and Future Work

  • Free tier cold starts. Render's free web service sleeps after 15 minutes of inactivity; the first request afterward can take 30 to 50 seconds.
  • No password reset flow. Signup and login only, no email verification or recovery.
  • No pagination. /api/posts/ returns the full list; fine at small scale, would need pagination or infinite scroll for a larger dataset.
  • No comments or likes. The data model is intentionally minimal: a post belongs to a user, nothing else.
  • No image uploads. Posts are text only.
  • Third party cookie blocking. Some browsers, Safari by default and others with strict tracking protection, may still block cross site cookies entirely regardless of SameSite=None. A production hardened version would likely move to token based auth, such as DRF's TokenAuthentication or JWT, to remove the dependency on cross site cookies altogether.

Contributors

TiwariDivya25

Issues