Skip to content

Ashutosh-Vijay/pincode-db-api

Repository files navigation

PincodeDB API

A serverless, multi-tenant REST API that turns any CSV of postal codes into a queryable, secure, private database - deployed entirely on AWS with zero servers to manage.

Status: This project was built as a personal project, deployed live on AWS via RapidAPI, and has since been taken down. The infrastructure-as-code, application logic, and test suite in this repository are the complete, production-ready source.


The Problem

Companies often maintain their own postal/pincode master data in spreadsheets - custom delivery zones, regional risk scores, serviceability flags, SLA tiers. When a backend team needs this data available as an API, the options are:

  1. Build and host your own lookup service (DynamoDB + Lambda + API Gateway plumbing, user isolation, quota management, etc.)
  2. Use a public postal code API that knows nothing about your business-specific columns.

PincodeDB is option 3: upload your CSV, get a private, queryable API back in seconds. Your data stays isolated. Nobody else can see or query it. You can also fall back to a global public Indian pincode dataset (~155K records) when your private data doesn't have a match.


Architecture

The entire backend is 100% serverless on AWS, defined in a single template.yaml (AWS SAM / CloudFormation).

Architecture Diagram

High-Level Architecture

flowchart TB
    Client["Client / RapidAPI Marketplace"]

    subgraph API["API Gateway (REST + OpenAPI 3.0)"]
        direction LR
        Auth["Lambda Authorizer\n(JIT User Provisioning)"]
    end

    subgraph Lambdas["Lambda Functions (Python 3.13, ARM64)"]
        direction LR
        Health["Health"]
        Lookup["Lookup"]
        Batch["Batch Lookup"]
        Strategy["Search Strategy"]
        Upload["Upload"]
        ListDS["List Datasets"]
        Status["Get Status"]
        Delete["Delete Dataset"]
    end

    subgraph Storage["Storage Layer"]
        direction LR
        DDB_Pincode[("DynamoDB\nPincodeTable\n(GSI: Pincode, Dataset)")]
        DDB_Users[("DynamoDB\nPincodeUsers\n(GSI: Marketplace, Tier)")]
        DDB_Datasets[("DynamoDB\nPincodeDatasets\n(GSI: Owner)")]
        S3["S3 Upload Bucket\n(AES-256, Versioned)"]
    end

    subgraph Ingestion["Async Ingestion Pipeline"]
        S3Proc["S3 Processor Lambda\n(CSV Parse + Validate + Ingest)"]
    end

    subgraph Ops["Observability & Operations"]
        direction LR
        CW["CloudWatch\nAlarms + Metrics"]
        DLQ["SQS Dead-Letter\nQueues"]
        SNS["SNS Ops Topic"]
        SSM["SSM Parameter Store\n(Kill Switch + Secrets)"]
        XRay["X-Ray Tracing"]
    end

    subgraph Scheduled["Scheduled Jobs"]
        Reset["Reset Usage Lambda\n(Monthly Cron)"]
    end

    Client -->|"HTTPS"| API
    Auth -->|"Allow/Deny + Context"| Lambdas
    Auth -.->|"Read/Write"| DDB_Users
    Auth -.->|"Read"| SSM

    Lookup & Batch & Strategy -->|"Query"| DDB_Pincode
    Upload -->|"Presigned POST"| S3
    Upload -.->|"Read"| DDB_Users
    ListDS & Status -->|"Query"| DDB_Datasets
    Delete -->|"Delete"| DDB_Pincode
    Delete -->|"Delete"| DDB_Datasets
    Delete -.->|"Update"| DDB_Users

    S3 -->|"S3 Event Trigger\n(ObjectCreated: *.csv)"| S3Proc
    S3Proc -->|"Batch Write"| DDB_Pincode
    S3Proc -->|"Update Status"| DDB_Datasets
    S3Proc -.->|"Update Quotas"| DDB_Users

    Reset -->|"Reset Counters"| DDB_Users

    Lambdas -.->|"Metrics"| CW
    S3Proc -.->|"Metrics"| CW
    Lambdas -.->|"Failed Events"| DLQ
    CW -.->|"Alarm"| SNS
    Lambdas -.->|"Traces"| XRay

    style Client fill:#f9f,stroke:#333,stroke-width:2px
    style API fill:#ff9900,stroke:#333,stroke-width:2px,color:#000
    style Storage fill:#3b48cc,stroke:#333,stroke-width:2px,color:#fff
    style Ingestion fill:#2d8f2d,stroke:#333,stroke-width:2px,color:#fff
    style Ops fill:#666,stroke:#333,stroke-width:2px,color:#fff
    style Scheduled fill:#8B4513,stroke:#333,stroke-width:2px,color:#fff
Loading

Upload & Ingestion Flow

sequenceDiagram
    participant C as Client
    participant AG as API Gateway
    participant UL as Upload Lambda
    participant S3 as S3 Bucket
    participant SP as S3 Processor Lambda
    participant DB as DynamoDB

    C->>AG: POST /postal-code/upload
    AG->>UL: Invoke (with auth context)
    UL->>DB: Check user tier & quotas
    UL-->>C: Presigned S3 POST URL + datasetId

    C->>S3: Direct upload (multipart/form-data)
    S3-->>C: HTTP 204 No Content

    S3--)SP: S3 Event Trigger (ObjectCreated)
    SP->>S3: Get CSV object
    SP->>SP: Parse CSV, validate schema,<br/>check quotas
    SP->>DB: Batch write pincode records
    SP->>DB: Update dataset status = SUCCEEDED
    SP->>DB: Update user quotas

    loop Poll until complete
        C->>AG: GET /dataset/{id}/status
        AG->>DB: Read dataset record
        DB-->>C: {status: PROCESSING | SUCCEEDED | FAILED}
    end
Loading

AWS Services Used

Service Purpose
API Gateway REST API with OpenAPI 3.0 spec, Lambda authorizer for auth
Lambda (Python 3.13, ARM64) 10 functions - auth, lookup, batch lookup, search strategy, upload, S3 processor, delete, status, list datasets, usage reset
DynamoDB 3 tables with 7 GSIs total for multi-access-pattern queries
S3 Encrypted upload bucket with presigned POST URLs, lifecycle policies
SSM Parameter Store Kill switch and proxy secret storage
SQS Dead-letter queues for failed invocations (ingest DLQ + general DLQ)
CloudWatch Alarms on Lambda errors and DLQ depth, embedded custom metrics
SNS Ops alert topic wired to alarms
X-Ray Distributed tracing enabled on all functions
EventBridge Monthly cron to reset usage counters for paid tiers

Key Design Decisions

Multi-Tenant Data Isolation

All postal code records share a single DynamoDB table (PincodeTable), partitioned by internalUserId. A user's queries can only ever hit their own partition. The table schema:

  • Partition Key: internalUserId (tenant identifier)
  • Sort Key: pincode_officename (composite: {pincode}#{officeName})
  • GSI PincodeIndex: internalUserId + pincode - for "find this pincode across all my datasets"
  • GSI DatasetIndex: datasetId - for "find all records in this specific dataset"

This means a single table serves all tenants with zero cross-tenant data leakage, while supporting three different query patterns efficiently.

JIT (Just-In-Time) User Provisioning

There is no separate signup or onboarding flow. The Lambda authorizer handles everything:

  1. Validates the RapidAPI proxy secret (via SSM Parameter Store)
  2. Looks up the user by their RapidAPI marketplace ID
  3. If the user doesn't exist, creates them on the spot with the correct tier
  4. If the user exists but their subscription tier has changed (upgrade/downgrade), updates them immediately
  5. Returns an IAM policy + context (userId, tier, clientId) that downstream Lambdas consume

This eliminates an entire class of "user sync" problems between the marketplace and the backend.

Secure Upload Flow (Presigned POST)

Users never send CSV data through API Gateway (which has a 10MB payload limit and would waste Lambda execution time). Instead:

  1. POST /postal-code/upload returns a presigned S3 POST with size limits enforced by the presigned policy (tied to the user's tier)
  2. The client uploads directly to S3 (server-side encryption enforced)
  3. An S3 event trigger fires the S3 Processor Lambda, which parses the CSV, validates it, and writes to DynamoDB
  4. The user polls GET /dataset/{id}/status until processing completes

This keeps the upload path off the API Gateway entirely and lets S3 handle the heavy lifting.

Structured vs. Dynamic Schema

Lower tiers (FREE, BUSINESS) require a structured CSV with known columns (pincode, country, area/officeName, district, state, etc.). The S3 processor normalizes headers (e.g., zip_code -> pincode, province -> stateName) and maps them to a canonical schema.

Higher tiers (GROWTH, ENTERPRISE) unlock dynamic schema: upload any CSV with a pincode column and arbitrary extra columns. The extra columns are stored as a customData map in DynamoDB and returned as-is in query responses. This means a logistics company can upload pincode,delivery_sla,risk_score,is_serviceable and query all of it back without any schema definition.

Tiered Pricing Model

Defined in shared/tier.py and enforced across upload, ingestion, and query:

Tier RapidAPI Plan Max File Size Max Total Rows Max Datasets Dynamic Schema
FREE BASIC 50 KB 250 1 No
BUSINESS PRO 10 MB 1,000,000 50 No
GROWTH ULTRA 20 MB 5,000,000 200 Yes
ENTERPRISE MEGA 50 MB 25,000,000 1,000 Yes

FREE tier users get exactly one upload (enforced via one_time_upload_used flag). Paid tier usage counters reset monthly via a scheduled Lambda.


Security

  • Proxy secret validation - Every request is verified against a secret shared with RapidAPI, stored in SSM Parameter Store (SecureString). Prevents direct API Gateway access bypassing the marketplace.
  • Tenant isolation - All DynamoDB queries are scoped to internalUserId. Ownership checks on dataset delete and status endpoints.
  • S3 hardening - Public access fully blocked, HTTPS-only (deny insecure transport), AES-256 server-side encryption enforced on every upload, versioning enabled, DeleteObject denied on raw uploads via IAM policy.
  • Kill switch - An SSM parameter (/pincode-api/enabled) that, when set to false, makes the authorizer deny all requests. Instant global shutoff without a deployment.

API Endpoints

Method Path Auth Description
GET /health Public Health check
GET /postal-code/search Required Lookup a single pincode (all datasets or a specific one, with optional public fallback)
POST /postal-code/batch-search Required Batch lookup up to 100 pincodes in one call
POST /postal-code/search-strategy Required (GROWTH+) Chained search with custom priority order (specific dataset -> all private -> public)
POST /postal-code/upload Required Get a presigned URL to upload a CSV
GET /datasets Required List all your uploaded datasets
GET /dataset/{datasetId}/status Required Check processing status of a dataset
DELETE /dataset/{datasetId} Required (BUSINESS+) Delete a private dataset and its records

The full OpenAPI 3.0 specification is embedded in template.yaml.


Observability

Every user-facing Lambda emits custom CloudWatch metrics via aws-embedded-metrics:

  • LatencyMs - end-to-end handler latency
  • RequestsOk / RequestsError - success/failure counters
  • RowsIngested - rows written per ingestion
  • RequestedPins - batch size per batch lookup
  • SkippedTriggers - duplicate S3 events that were safely ignored

CloudWatch Alarms are configured for:

  • Lookup Lambda errors (fires on any error within a 60s window)
  • Ingest DLQ depth (fires if any message lands in the dead-letter queue)

Both route to an SNS topic for alerting.


Project Structure

pincode-db-api/
  src/
    auth/app.py              # Lambda authorizer (JIT user provisioning + tier sync)
    lookup/app.py            # Single pincode lookup with fallback
    batch_lookup/app.py      # Batch lookup (up to 100 pincodes)
    search_strategy/app.py   # Custom priority chain search (premium)
    upload/app.py            # Presigned S3 POST URL generation
    s3handler/app.py         # S3 event processor (CSV parse, validate, ingest)
    delete_dataset/app.py    # Dataset deletion with quota adjustment
    get_status/app.py        # Dataset processing status check
    list_datasets/app.py     # List user's datasets
    reset_usage/app.py       # Monthly usage counter reset (scheduled)
    health/app.py            # Health check
  shared/
    tier.py                  # Tier definitions and limits
  tests/
    conftest.py              # Shared fixtures (moto mock AWS, DynamoDB table setup)
    test_*.py                # Test suite (~40 tests covering all Lambdas)
  template.yaml              # SAM/CloudFormation template (entire infrastructure)
  ingest_public_data.py      # One-time script to load public Indian pincode data

Testing

Tests use moto to mock AWS services (DynamoDB, S3, SSM) in-process. No real AWS calls are made during testing.

# Setup
python -m venv .venv
source .venv/bin/activate      # or .\.venv\Scripts\Activate.ps1 on Windows
pip install -r requirements.txt
pip install -r tests/requirements-dev.txt

# Run tests with coverage
pytest

Coverage spans all Lambda handlers including happy paths, authorization failures, quota enforcement, encoding errors, race condition handling, and pagination.

CI runs on every push via GitHub Actions: lint (ruff) -> format check (black) -> tests + coverage -> SAM validate.


Deployment

Note: This project was previously deployed and has since been taken down. These instructions are preserved for reference.

# Build
sam build

# Deploy (interactive first time)
sam deploy --guided

The SAM template creates all resources (API Gateway, 10 Lambdas, 3 DynamoDB tables, S3 bucket, SQS queues, CloudWatch alarms, SNS topic, SSM parameters) in a single stack. DynamoDB tables and the S3 bucket have DeletionPolicy: Retain to prevent accidental data loss on stack deletion.

About

A DB-as-a-Service for Pincodes built with AWS SAM.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors