Skip to content

saumya66/onepdf-backend

Repository files navigation

FastAPI Backend Template

A robust and scalable backend template built with FastAPI, SQLAlchemy, and PostgreSQL. This template provides a solid foundation for building modern web applications with features like authentication, database integration, and API documentation.

Features

  • FastAPI: High-performance web framework
  • SQLAlchemy: SQL toolkit and ORM
  • PostgreSQL: Robust relational database
  • Alembic: Database migration tool
  • JWT Authentication: Secure authentication system with bcrypt password hashing
  • Docker: Containerization for easy deployment
  • Poetry: Dependency management
  • Pre-commit hooks: Code quality tools

Security & Authentication

Password Hashing with Passlib & Bcrypt

This application uses passlib with bcrypt as the hashing algorithm for secure password storage.

Why Two Libraries?

  • passlib - High-level password hashing library that provides a clean, consistent API
  • bcrypt - The actual cryptographic backend that performs the hashing

Think of passlib as the interface and bcrypt as the engine doing the work.

Configuration

In app/core/security.py:

from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

This setup provides:

  • Algorithm flexibility: Easy to upgrade to stronger algorithms without code changes
  • Auto-rehashing: Automatically upgrades weak hashes to stronger ones
  • Clean API: Simple hash() and verify() methods
  • Future-proof: Can support multiple algorithms simultaneously

Version Compatibility

Important: We pin bcrypt to version 4.0.1 to maintain compatibility with passlib 1.7.4:

passlib = {extras = ["bcrypt"], version = "^1.7.4"}
bcrypt = "4.0.1"

Newer versions of bcrypt (4.1+) have breaking changes that cause issues with passlib. This pinning ensures stability.

Usage Example

from app.core.security import get_password_hash, verify_password

# Hashing a password
hashed = get_password_hash("my_secure_password")

# Verifying a password
is_valid = verify_password("my_secure_password", hashed)

JWT Tokens

Access tokens are created using python-jose with HS256 algorithm:

from app.core.security import create_access_token

token = create_access_token(subject=user.email)

Token expiration is configured via ACCESS_TOKEN_EXPIRE_MINUTES in your .env file (default: 8 days).

Project Structure

backend/
├── alembic/              # Database migrations
├── app/
│   ├── api/              # API endpoints
│   │   └── api_v1/       # API version 1
│   │       └── endpoints/# API endpoint modules
│   ├── core/             # Core functionality and config
│   ├── db/               # Database models and sessions
│   ├── models/           # SQLAlchemy models
│   ├── schemas/          # Pydantic models
│   ├── services/         # Business logic services
│   └── main.py           # FastAPI application
├── .env                  # Environment variables (create from .env.example)
├── .env.example          # Example environment variables
├── docker-compose.yml    # Docker Compose configuration
├── Dockerfile            # Docker configuration
├── pyproject.toml        # Project dependencies
├── setup.sh              # Setup script
└── README.md             # This file

Getting Started

Prerequisites

Installation

  1. Clone the repository
git clone https://github.com/yourusername/backend-template.git
cd backend-template
  1. Run the setup script
chmod +x setup.sh
./setup.sh

This will:

  • Create a virtual environment and install dependencies
  • Create necessary directories
  • Create a .env file from .env.example
  • Run database migrations
  1. Start the application
poetry run uvicorn app.main:app --reload       

The API will be available at http://localhost:8000

API documentation will be available at http://localhost:8000/docs

Docker Deployment

To build and run the application using Docker:

docker compose up -d --build

To shutdown the application using Docker:

docker compose down

Development

To add a new package

poetry add <package_name>

To remove a package

poetry remove <package_name>

Running Tests

poetry run pytest

Running Migrations

Create a new migration:

poetry run alembic revision --autogenerate -m "Description of changes"

Apply migrations:

poetry run alembic upgrade head

If curious about working of alembic:

Great question! Let me explain how Alembic is configured and works in your codebase:

Alembic Overview

Alembic is a database migration tool that tracks and applies changes to your database schema over time. Think of it as "version control for your database."

How It's Set Up in Your Codebase

1. Configuration Files

alembic.ini - Main configuration file

  • Points to the alembic/ directory
  • Sets up logging and other basic settings

alembic/env.py - The environment/runtime configuration

Let me show you the key parts:

from alembic import context
from sqlalchemy import engine_from_config, pool

from app.core.config import settings
# Import the model registry to ensure all models are loaded
from app.db.base_class import Base  # Correctly import from base_class
import app.models.base  # Import the base model aggregator

This imports your settings and models.

target_metadata = Base.metadata

# Override sqlalchemy.url using discrete settings
database_url = (
    f"postgresql://{settings.POSTGRES_USER}:{settings.POSTGRES_PASSWORD}"
    f"@{settings.POSTGRES_SERVER}:{settings.POSTGRES_PORT}/{settings.POSTGRES_DB}"
)
config.set_main_option("sqlalchemy.url", database_url)

Key points here:

  • target_metadata = Base.metadata - This tells Alembic to look at your SQLAlchemy models
  • The database URL is built from your .env variables (this is why changing the .env changes which database it connects to)

2. How Models Are Discovered

from app.db.base_class import Base
from .user import User
from .conversation import Conversation
from .message import Message
from .file import File

This file imports all your models. When alembic/env.py imports app.models.base, it loads all your model classes.

3. Your Models

Each model defines a database table:

class User(Base):
    __tablename__ = "users"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    email = Column(Text, unique=True, nullable=False)
    name = Column(Text, nullable=True)
    hashed_password = Column(Text, nullable=False)
    created_at = Column(DateTime(timezone=True), server_default=func.now())
    updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())

    # Relationships
    conversations = relationship("Conversation", back_populates="user", cascade="all, delete-orphan")
    files = relationship("File", back_populates="uploaded_by_user", cascade="all, delete-orphan")

Alembic Workflow

1. Generate Migration (Autogenerate)

poetry run alembic revision --autogenerate -m "Description of change"

What happens:

  1. Alembic connects to your database (using .env credentials)
  2. Reads the current database schema
  3. Compares it to your SQLAlchemy models (app/models/*.py)
  4. Detects differences (new tables, new columns, removed columns, etc.)
  5. Generates a migration file in alembic/versions/

Migration file structure:

revision = 'abc123'  # Unique ID
down_revision = 'xyz789'  # Previous migration (creates a chain)

def upgrade() -> None:
    # SQL commands to apply changes
    op.create_table('users', ...)
    
def downgrade() -> None:
    # SQL commands to revert changes
    op.drop_table('users')

2. Apply Migrations

poetry run alembic upgrade head

What happens:

  1. Checks which migrations have been applied (stored in alembic_version table in your DB)
  2. Runs the upgrade() function of each pending migration in order
  3. Updates the alembic_version table

3. Check Current Status

poetry run alembic current

Shows which migration is currently applied.

poetry run alembic history

Shows all migrations and their chain.

4. Rollback

poetry run alembic downgrade -1

Reverts the last migration by running its downgrade() function.

Your Current Situation

alembic/versions/
└── [NEW_ID]_create_initial_tables.py  # Just generated

This migration will create 4 tables:

  • users - User accounts
  • conversations - Chat conversations
  • messages - Messages within conversations
  • files - Uploaded PDF files

Relationships:

users (1) ─── (many) conversations
conversations (1) ─── (many) messages
conversations (1) ─── (many) files
users (1) ─── (many) files

Common Commands

Command What it does
alembic revision --autogenerate -m "msg" Generate new migration
alembic upgrade head Apply all pending migrations
alembic current Show current migration version
alembic history Show migration history
alembic downgrade -1 Rollback last migration
alembic downgrade base Rollback all migrations

Best Practices in Your Codebase

  1. Always change models first - Modify app/models/*.py
  2. Then generate migration - Run alembic revision --autogenerate
  3. Review the migration - Check the generated file is correct
  4. Apply to database - Run alembic upgrade head
  5. Commit both - Model changes + migration file to git

Why This Matters

Without Alembic, you'd have to:

  • Manually write SQL CREATE/ALTER statements
  • Track which changes were applied to which database
  • Coordinate schema changes across team members
  • Risk database inconsistencies

With Alembic:

  • Schema changes are versioned
  • Can deploy to production safely
  • Can rollback if needed
  • Team stays in sync

Now you can run:

poetry run alembic upgrade head

This will create all your tables in Supabase! 🚀

Code Quality

Install pre-commit hooks:

poetry run pre-commit install

Chat API Endpoints

The application provides a comprehensive set of APIs for building a chat interface. All endpoints require authentication via the access_token header.

1. Send Message / Create Chat

POST /api/v1/chat/

Send a message in a conversation. Creates a new conversation if conversation_id is not provided.

Headers:

  • access_token: JWT authentication token

Form Data:

  • user_message (required): The user's message text
  • conversation_id (optional): UUID of existing conversation
  • pdfs (optional): List of PDF files to upload
  • images (optional): List of image files to upload

Response:

{
  "conversation_id": "uuid",
  "assistant_response": {
    "reply_text_message": "string",
    "should_trigger_workflow": false,
    "workflow": []
  },
  "response_metadata": {},
  "usage_metadata": {}
}

2. Create New Conversation

POST /api/v1/chat/new-conversation

Create a new empty conversation.

Headers:

  • access_token: JWT authentication token

Request Body:

{
  "title": "My New Chat",
  "mode": "workflow"
}

Both fields are optional. If not provided, defaults to title: "New Chat" and mode: null.

Response:

{
  "id": "uuid",
  "user_id": "uuid",
  "title": "My New Chat",
  "mode": "workflow",
  "created_at": "datetime",
  "updated_at": "datetime"
}

3. Get All Conversations

GET /api/v1/chat/conversations

Retrieve all conversations for the authenticated user, sorted by most recent first.

Headers:

  • access_token: JWT authentication token

Query Parameters:

  • skip (optional, default: 0): Number of conversations to skip (pagination)
  • limit (optional, default: 100, max: 100): Maximum conversations to return

Response:

[
  {
    "id": "uuid",
    "user_id": "uuid",
    "title": "string",
    "mode": "string",
    "created_at": "datetime",
    "updated_at": "datetime",
    "messages": [],
    "files": []
  }
]

4. Get Specific Conversation

GET /api/v1/chat/conversations/{conversation_id}

Retrieve a specific conversation with all messages and files.

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • conversation_id: UUID of the conversation

Response:

{
  "id": "uuid",
  "user_id": "uuid",
  "title": "string",
  "mode": "string",
  "created_at": "datetime",
  "updated_at": "datetime",
  "messages": [
    {
      "id": "uuid",
      "conversation_id": "uuid",
      "sender": "user|assistant",
      "message": "string or object",
      "created_at": "datetime"
    }
  ],
  "files": []
}

5. Get Conversation Messages (Paginated)

GET /api/v1/chat/conversations/{conversation_id}/messages

Get paginated messages for a specific conversation.

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • conversation_id: UUID of the conversation

Query Parameters:

  • skip (optional, default: 0): Number of messages to skip
  • limit (optional, default: 50, max: 100): Maximum messages to return

Response:

{
  "messages": [
    {
      "id": "uuid",
      "conversation_id": "uuid",
      "sender": "user|assistant",
      "message": "string or object",
      "response_metadata": {},
      "usage_metadata": {},
      "created_at": "datetime"
    }
  ],
  "total": 100,
  "skip": 0,
  "limit": 50
}

6. Update Conversation

PATCH /api/v1/chat/conversations/{conversation_id}

Update conversation details (e.g., title or mode).

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • conversation_id: UUID of the conversation

Request Body:

{
  "title": "New Title",
  "mode": "workflow"
}

Response:

{
  "id": "uuid",
  "user_id": "uuid",
  "title": "New Title",
  "mode": "workflow",
  "created_at": "datetime",
  "updated_at": "datetime",
  "messages": [],
  "files": []
}

7. Delete Conversation

DELETE /api/v1/chat/conversations/{conversation_id}

Delete a conversation and all associated messages and files.

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • conversation_id: UUID of the conversation

Response:

{
  "message": "Conversation deleted successfully",
  "conversation_id": "uuid"
}

8. Delete Specific Message

DELETE /api/v1/chat/conversations/{conversation_id}/messages/{message_id}

Delete a specific message from a conversation.

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • conversation_id: UUID of the conversation
  • message_id: UUID of the message to delete

Response:

{
  "message": "Message deleted successfully",
  "conversation_id": "uuid",
  "message_id": "uuid"
}

9. Get Conversation Files

GET /api/v1/chat/conversations/{conversation_id}/files

Get all files uploaded in a specific conversation.

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • conversation_id: UUID of the conversation

Query Parameters:

  • skip (optional, default: 0): Number of files to skip
  • limit (optional, default: 100, max: 100): Maximum files to return

Response:

[
  {
    "id": "uuid",
    "conversation_id": "uuid",
    "uploaded_by": "uuid",
    "filename": "document.pdf",
    "mime_type": "application/pdf",
    "file_size": 1024000,
    "page_count": 10,
    "created_at": "datetime"
  }
]

10. Delete Conversation File

DELETE /api/v1/chat/conversations/{conversation_id}/files/{file_id}

Delete a specific file from a conversation.

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • conversation_id: UUID of the conversation
  • file_id: UUID of the file

Response:

{
  "message": "File deleted successfully",
  "conversation_id": "uuid",
  "file_id": "uuid"
}

11. Download File

GET /api/v1/chat/files/{file_id}/download

Download a file by its ID. Returns the file with appropriate headers for browser download.

Headers:

  • access_token: JWT authentication token

Path Parameters:

  • file_id: UUID of the file to download

Response:

  • Returns binary file data with headers:
    • Content-Type: The file's MIME type (e.g., application/pdf)
    • Content-Disposition: attachment; filename="filename.pdf"
    • Content-Length: File size in bytes

Usage Example (JavaScript):

// Fetch and trigger download
fetch(`/api/v1/chat/files/${fileId}/download`, {
  headers: { 'access_token': token }
})
.then(response => response.blob())
.then(blob => {
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'filename.pdf';
  a.click();
});

Error Responses

All endpoints may return the following error responses:

  • 401 Unauthorized: Invalid or missing access token
  • 403 Forbidden: User not authorized to access the resource
  • 404 Not Found: Resource not found
  • 400 Bad Request: Invalid request parameters
  • 500 Internal Server Error: Server error

Example error response:

{
  "detail": "Invalid access token"
}

Razorpay Subscriptions Integration

This application includes a complete Razorpay subscription system for handling recurring payments.

Architecture

The payment system is designed to be modular and reusable:

app/
├── services/
│   ├── razorpay_service.py      # All Razorpay API calls (reusable)
│   └── subscription_service.py  # Local DB operations & limits
├── schemas/
│   ├── payment.py               # Payment request/response schemas
│   └── subscription.py          # Plan & subscription schemas
└── api/v1/
    ├── subscriptions.py         # Subscription endpoints
    └── webhooks.py              # Razorpay webhook handler

Environment Variables

RAZORPAY_KEY_ID=rzp_test_xxxxx
RAZORPAY_KEY_SECRET=xxxxx
RAZORPAY_WEBHOOK_SECRET=xxxxx

API Endpoints

Endpoint Method Description
/api/v1/subscriptions/plans GET List available plans
/api/v1/subscriptions/create POST Create Razorpay subscription
/api/v1/subscriptions/verify POST Verify payment signature
/api/v1/subscriptions/current GET Get user's subscription
/api/v1/subscriptions/cancel POST Cancel subscription
/api/v1/webhooks/razorpay POST Razorpay webhook handler

Subscription Flow

Month 1 (First Payment) - Complete Flow

┌─────────────────────────────────────────────────────────────────┐
│ Step 1: User clicks "Upgrade to Pro"                            │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────────┐
│ POST /api/v1/subscriptions/create                              │
│ • Validates plan exists                                         │
│ • Creates subscription in Razorpay                              │
│ • Creates UserSubscription (status: "created")                  │
│ • Returns: { subscription_id, key_id, amount }                  │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────────┐
│ Frontend: Razorpay Checkout                                    │
│ • Opens checkout with subscription_id                          │
│ • User enters payment details (card/UPI)                        │
│ • User completes payment                                       │
│ • Returns: { payment_id, subscription_id, signature }           │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────────────┐
│ POST /api/v1/subscriptions/verify                              │
│ • Verifies payment signature                                    │
│ • Creates Payment entry (with user_id & subscription_id)       │
│ • Sets subscription status to "active"                          │
│ • Unlocks Pro features immediately                              │
│ • Returns: { success: true, message: "..." }                    │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │   Webhooks Fire           │
        │   (May arrive in any      │
        │    order, async)          │
        └───────────────────────────┘
                    │
        ┌───────────┴───────────┐
        │                       │
        ▼                       ▼
┌──────────────────┐   ┌──────────────────┐
│ payment.authorized│   │ payment.captured  │
│ (Optional)        │   │                  │
│ • Logging only    │   │ • If exists:      │
│                   │   │   - Update status│
│                   │   │   - Store entity │
│                   │   │ • If not exists: │
│                   │   │   - Create entry │
│                   │   │   - No user/sub  │
└──────────────────┘   └──────────────────┘
        │                       │
        └───────────┬───────────┘
                    │
        ┌───────────┴───────────┐
        │                       │
        ▼                       ▼
┌──────────────────┐   ┌──────────────────┐
│ subscription.    │   │ subscription.     │
│ authenticated    │   │ activated        │
│                  │   │                  │
│ • Status:        │   │ • Status:        │
│   "authenticated"│   │   "active"        │
│                  │   │ • Update period   │
│                  │   │   dates           │
│                  │   │ • Idempotent      │
└──────────────────┘   └──────────────────┘
        │                       │
        └───────────┬───────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │ subscription.charged       │
        │                           │
        │ • Status: "active"         │
        │ • Update period dates      │
        │ • Link payment to sub      │
        │   (if no subscription_id)   │
        │ • Add to payments list     │
        └───────────────────────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │ invoice.paid              │
        │                           │
        │ • Store invoice entity in │
        │   payment.invoice_details │
        └───────────────────────────┘

Month 2+ (Recurring Payments) - Complete Flow

┌─────────────────────────────────────────────────────────────────┐
│ Razorpay automatically charges user at billing cycle            │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │   Webhooks Fire           │
        │   (Async, may arrive in   │
        │    any order)             │
        └───────────────────────────┘
                    │
        ┌───────────┴───────────┐
        │                       │
        ▼                       ▼
┌──────────────────┐   ┌──────────────────┐
│ payment.captured  │   │ subscription.    │
│                  │   │ charged          │
│ • If exists:      │   │                  │
│   - Update status │   │ • Status:        │
│   - Store entity  │   │   "active"        │
│ • If not exists:  │   │ • Update period   │
│   - Create entry  │   │   dates           │
│   - No user/sub   │   │ • Link payment   │
│   - Store entity  │   │   to subscription │
│                   │   │ • Add to payments │
└──────────────────┘   │   list            │
        │               └──────────────────┘
        │                       │
        └───────────┬───────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │ invoice.paid              │
        │                           │
        │ • Store invoice entity in │
        │   payment.invoice_details │
        └───────────────────────────┘

Payment Failed Flow

┌─────────────────────────────────────────────────────────────────┐
│ Payment fails during checkout or recurring charge               │
└─────────────────────────────────────────────────────────────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │ payment.failed            │
        │                           │
        │ • If payment exists:      │
        │   - Update status:        │
        │     "failed"              │
        │   - Store payment entity  │
        │     with error details    │
        │ • If not exists:          │
        │   - Create entry           │
        │   - Status: "failed"      │
        │   - No user/sub linking   │
        │   - Store entity + errors  │
        └───────────────────────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │ subscription.pending      │
        │ (if retrying)             │
        │                           │
        │ • Status: "past_due"      │
        └───────────────────────────┘
                    │
                    ▼
        ┌───────────────────────────┐
        │ subscription.halted       │
        │ (if all retries fail)     │
        │                           │
        │ • Status: "halted"        │
        └───────────────────────────┘

Payment Entry Creation & Linking

First Payment:

  • Created in /verify API with user_id and subscription_id linked
  • Status: "success"
  • Linked to subscription immediately

Recurring Payments:

  • Created in payment.captured webhook (without user_id/subscription_id)
  • Status: "success"
  • Linked to subscription in subscription.charged webhook:
    • Finds payment by razorpay_payment_id
    • Links if subscription_id and user_id are None
    • Adds to subscription.payments collection

Failed Payments:

  • Created/updated in payment.failed webhook
  • Status: "failed"
  • Stores entire payment entity with error details in notes
  • No user_id/subscription_id linking

Detailed Payment Flow

First Payment Flow (Step-by-Step)

  1. User initiates subscription (POST /api/v1/subscriptions/create)

    • Creates subscription in Razorpay
    • Creates local UserSubscription entry with status "created"
    • Returns subscription_id for frontend
  2. User completes Razorpay checkout

    • Frontend opens Razorpay Checkout
    • User enters payment details and completes payment
    • Frontend receives: razorpay_payment_id, razorpay_subscription_id, razorpay_signature
  3. Verify payment (POST /api/v1/subscriptions/verify)

    • Verifies payment signature
    • Creates Payment entry if it doesn't exist:
      • Links to user (user_id)
      • Links to subscription (subscription_id)
      • Status: "success"
    • Sets subscription status to "active" immediately
    • Unlocks Pro features right away
  4. Webhooks arrive (async, may arrive in any order):

    • payment.authorized (optional): Only logging, no DB operations
    • payment.captured:
      • If payment exists: Updates status to "success", stores payment entity in notes
      • If payment doesn't exist: Creates payment entry (without user_id/subscription_id), stores payment entity in notes
    • subscription.authenticated: Updates subscription status to "authenticated"
    • subscription.activated: Updates subscription status to "active" (idempotent), updates period dates
    • subscription.charged:
      • Updates subscription status to "active"
      • Updates period dates
      • Links payment to subscription (if payment has no subscription_id/user_id)
      • Adds payment to subscription.payments collection
    • invoice.paid: Stores entire invoice entity in payment.invoice_details

Recurring Payment Flow (Month 2+)

  1. Razorpay auto-charges user at billing cycle

    • No frontend interaction needed
    • Razorpay automatically processes payment
  2. Webhooks arrive (async):

    • payment.captured:
      • If payment exists: Updates status to "success", stores payment entity in notes
      • If payment doesn't exist: Creates payment entry (without user_id/subscription_id), stores payment entity in notes
    • subscription.charged:
      • Updates subscription status to "active"
      • Updates period dates
      • Links payment to subscription (if payment has no subscription_id/user_id)
      • Adds payment to subscription.payments collection
    • invoice.paid: Stores entire invoice entity in payment.invoice_details

Payment Failed Flow

  1. Payment fails (during checkout or recurring charge)

    • payment.failed webhook fires:
      • If payment exists: Updates status to "failed", stores payment entity with error details in notes
      • If payment doesn't exist: Creates payment entry with status "failed" (without user_id/subscription_id), stores payment entity with error details in notes
  2. Subscription status updates:

    • subscription.pending: Status → "past_due" (payment retrying)
    • subscription.halted: Status → "halted" (all retries exhausted)

Key Points

  • Payment Creation:

    • First payment: Created in /verify API with full linking
    • Recurring payments: Created in payment.captured webhook, then linked in subscription.charged
    • Failed payments: Created/updated in payment.failed webhook
  • Subscription Activation:

    • First payment: Activated immediately in /verify API (instant unlock)
    • Webhooks ensure eventual consistency
  • Payment Linking:

    • subscription.charged links payments that don't have subscription_id/user_id
    • Payments are added to subscription.payments collection
  • Data Storage:

    • Payment entity stored in payment.notes (from payment.captured)
    • Invoice entity stored in payment.invoice_details (from invoice.paid)

Webhook Events

Event Status Description
subscription.authenticated authenticated First payment authorized
subscription.activated active Subscription is now live
subscription.charged active Recurring payment successful
subscription.pending past_due Payment failed, retrying
subscription.halted halted All retries exhausted
subscription.paused paused Subscription paused
subscription.resumed active Resumed from pause
subscription.cancelled cancelled Subscription cancelled
subscription.completed completed All billing cycles done
payment.authorized - Payment authorized (logging only)
payment.captured - Payment captured successfully
payment.failed - Payment failed
invoice.paid - Invoice paid (stores invoice details)

Frontend Integration Example

async function upgradeToPro() {
  // 1. Create subscription
  const res = await fetch('/api/v1/subscriptions/create', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ plan_name: 'pro' })
  });
  
  const { subscription_id, key_id } = await res.json();
  
  // 2. Open Razorpay Checkout
  const options = {
    key: key_id,
    subscription_id: subscription_id,
    name: 'OnePDF',
    handler: async function(response) {
      // 3. Verify payment
      await fetch('/api/v1/subscriptions/verify', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          razorpay_payment_id: response.razorpay_payment_id,
          razorpay_subscription_id: response.razorpay_subscription_id,
          razorpay_signature: response.razorpay_signature
        })
      });
      
      // Success! Refresh user state
      alert('Welcome to Pro!');
    }
  };
  
  new Razorpay(options).open();
}

Setup Steps

  1. Create plan in Razorpay Dashboard

    • Go to Subscriptions → Plans → Create Plan
    • Set name, amount (₹499), period (monthly)
    • Copy the plan_id
  2. Update database with plan ID

    poetry run python seed_plans.py pro plan_xxxxx
  3. Configure webhook in Razorpay Dashboard

    • Settings → Webhooks → Add webhook
    • URL: https://your-domain.com/api/v1/webhooks/razorpay
    • Events: Select all subscription.* events
    • Copy webhook secret to .env
  4. Run migrations

    poetry run alembic upgrade head

Database Tables

  • subscription_plans: Plan definitions (free, pro, enterprise)
  • user_subscriptions: User's active subscription
  • payments: Payment history

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages