// serverless ยท multi-tenant ยท aws

PincodeDB
API.

A serverless, multi-tenant backend that turns any CSV of postal codes into a private, queryable REST API. Upload the CSV. Get a live API back in seconds. No server provisioning, no database setup, nobody else's data mixed with yours.

10
Lambda Functions
3
DynamoDB Tables
7
GSIs
155K
Public Records
~40
Tests
1
SAM Template
Python 3.13 AWS Lambda (ARM64) API Gateway DynamoDB S3 CloudFormation / SAM SSM Parameter Store SQS DLQ CloudWatch SNS X-Ray EventBridge RapidAPI pytest moto GitHub Actions

100% serverless on AWS. Entire infrastructure as code in a single SAM template.

PincodeDB full AWS serverless architecture diagram
1
Subscribe on RapidAPI
Get an API key. No registration call to the backend needed โ€” user provisioning happens on the first real request via the Lambda authorizer.
2
POST /postal-code/upload
API returns a presigned S3 POST URL and a datasetId. The CSV never passes through API Gateway โ€” no 10MB limit.
3
Client uploads directly to S3
File goes straight from client to S3. Size limits are enforced by the presigned policy โ€” enforced by AWS, not by application code.
4
S3 event triggers ingestion
S3 fires an ObjectCreated event. The processor Lambda parses the CSV, validates the schema, and batch-writes to DynamoDB. Quota counters update atomically.
5
Poll status โ†’ query
GET /dataset/{id}/status โ†’ SUCCEEDED. Then GET /postal-code/search?pincode=110001 โ€” your private data comes back instantly.

Companies โ€” especially logistics, e-commerce, and fintech โ€” maintain their own pincode master data in spreadsheets. These aren't raw post office records; they carry business-specific columns like delivery_sla, risk_score, is_serviceable, zone_type.

When a backend team needs this data as an API, the realistic options are: build and host your own lookup service, or use a public pincode API that knows nothing about your internal columns.

PincodeDB is option 3: upload the CSV, get a live queryable API in seconds. Private, isolated, yours.

Deployed ยท Then Retired
Built live on AWS. Listed on RapidAPI. Source is public.
This project was built, deployed live on AWS, and listed on the RapidAPI marketplace. It was taken down when the free tier expired. The entire production source โ€” infrastructure-as-code, all Lambda handlers, and the full test suite โ€” is open on GitHub. The interesting parts are the architecture decisions, not the domain.

Six decisions that made the system work correctly, not just work.

Auth / Provisioning
JIT User Provisioning โ€” No Signup Flow
The Lambda authorizer does everything on every request: validates the RapidAPI proxy secret, looks up the user, creates them if they don't exist, and syncs their tier if they've upgraded or downgraded. No onboarding webhook, no separate registration endpoint, no periodic sync job. Zero drift between marketplace state and backend state.
Storage / Upload
Presigned S3 Upload โ€” API Gateway Never Sees the File
The upload endpoint returns a presigned S3 POST URL. The client uploads directly to S3 โ€” API Gateway's 10MB limit never applies. S3 fires an event, the processor Lambda wakes up. The API itself never touches the raw CSV data. Size limits are enforced by the presigned policy, not application logic.
Multi-Tenancy
Single-Table Design โ€” Cross-Tenant Leakage Impossible at Query Level
All tenants share one DynamoDB table, partitioned by internalUserId. Three GSIs cover three access patterns. Cross-tenant isolation is enforced structurally at the query โ€” not by application-level filtering that could be bypassed. One table, all users, zero leakage.
Operations
Kill Switch via SSM
One SSM parameter: /pincode-api/enabled. Set it to false and the authorizer denies every request globally, instantly โ€” no deployment, no code change. The authorizer reads this on every request before any user-facing work happens.
Enterprise Tier
Dynamic CSV Schema
Growth/Enterprise tiers accept any CSV. Extra columns beyond the canonical schema are stored as a customData map in DynamoDB and returned as-is. A logistics company uploads pincode,sla,risk,is_serviceable with no schema pre-registration required.
Observability
Custom CloudWatch Metrics + X-Ray Traces
Every Lambda emits LatencyMs, RequestsOk, RowsIngested, RequestedPins via aws-embedded-metrics. X-Ray distributed tracing on all functions. SQS DLQ depth alarm wired to SNS for ops alerting.
src/auth/app.py โ€” JIT User Provisioning
def handler(event, context):
    # Every request: validate โ†’ lookup โ†’ provision/sync
    _validate_proxy_secret(event['authorizationToken'])

    marketplace_id = _extract_marketplace_id(event)
    current_tier   = _get_tier_from_headers(event)

    user = users_table.get_item(
        Key={'marketplaceId': marketplace_id}
    ).get('Item')

    if not user:
        # First request โ€” create user on the spot
        user = _provision_user(marketplace_id, current_tier)
    elif user['tier'] != current_tier:
        # Subscription changed โ€” sync immediately
        user = _sync_tier(user, new_tier=current_tier)

    # Downstream Lambdas receive userId + tier in context
    return _build_allow_policy(
        principal=user['internalUserId'],
        context={
            'userId': user['internalUserId'],
            'tier':   user['tier'],
        }
    )
shared/utils.py โ€” Kill Switch + Tenant Isolation
def is_api_enabled(ssm) -> bool:
    """
    One SSM parameter. Instant global shutoff.
    No deployment. No code change.
    """
    param = ssm.get_parameter(
        Name='/pincode-api/enabled'
    )
    return param['Parameter']['Value'] == 'true'


def query_pincodes(table, user_id: str,
                   pincode: str) -> list:
    """
    Single-table multi-tenancy.
    All tenants share one table โ€” partition key
    is internalUserId. Cross-tenant leakage is
    impossible at the DynamoDB query level.
    """
    return table.query(
        KeyConditionExpression=(
            Key('internalUserId').eq(user_id) &
            Key('pincode_officename').begins_with(
                pincode
            )
        )
    )['Items']
Function What it does
AuthorizerJIT user provisioning + tier sync on every request. Validates RapidAPI proxy secret.
LookupSingle pincode search โ€” queries user's own datasets with optional public fallback.
Batch LookupUp to 100 pincodes in one call. Same isolation guarantees as single lookup.
Search StrategyCustom priority chain: specific dataset โ†’ all private โ†’ public. Growth+ only.
UploadGenerates presigned S3 POST URL with tier-based size limits enforced by policy.
S3 ProcessorParses CSV, validates schema, batch-writes to DynamoDB, updates quotas + status.
List DatasetsReturns all datasets owned by the authenticated user.
Get StatusReturns processing status for a specific dataset: PROCESSING / SUCCEEDED / FAILED.
Delete DatasetDeletes records, metadata, and adjusts quota counters. BUSINESS+ only.
Reset UsageScheduled monthly via EventBridge โ€” resets paid tier usage counters.
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 GROWTH+ Chained search with custom priority order: specific dataset โ†’ all private โ†’ public
POST /postal-code/upload Required Get a presigned S3 POST URL to upload a CSV dataset
GET /datasets Required List all uploaded datasets for the authenticated user
GET /dataset/{datasetId}/status Required Check processing status of a dataset
DELETE /dataset/{datasetId} BUSINESS+ Delete a private dataset and all its records
Tier Max File Max Rows Max Datasets Dynamic Schema
FREE 50 KB 250 1 No
BUSINESS 10 MB 1,000,000 50 No
GROWTH 20 MB 5,000,000 200 Yes
ENTERPRISE 50 MB 25,000,000 1,000 Yes
Proxy Secret Validation
Every request verified against a shared secret stored in SSM (SecureString). Prevents direct API Gateway access bypassing the marketplace.
Tenant Isolation
All DynamoDB queries scoped to internalUserId. Ownership checks on delete and status operations. No way to reach another tenant's data.
S3 Hardening
Public access fully blocked. HTTPS-only policy. AES-256 server-side encryption enforced. DeleteObject denied on raw uploads via IAM.
Kill Switch
SSM parameter /pincode-api/enabled. Set to false โ†’ instant global shutoff. No deployment, no code change.
01
Lint
ruff โ€” fast Python linter, runs on every push.
02
Format
black โ€” enforced code style, fails CI on diff.
03
Tests
pytest with moto โ€” ~40 tests, no real AWS calls.
04
Coverage
Coverage report generated alongside tests on every run.
05
SAM Validate
sam validate โ€” template validation against CloudFormation schema.
Test Coverage
~40 Tests โ€” No Real AWS Calls
Tests use moto to mock AWS services (DynamoDB, S3, SSM) in-process. Covers happy paths, auth failures, quota enforcement, encoding edge cases, S3 duplicate event handling, and pagination.
Infrastructure as Code
One SAM Template โ€” All Resources
The entire stack โ€” API Gateway, 10 Lambdas, 3 DynamoDB tables, S3 bucket, SQS queues, CloudWatch alarms, SNS topic, SSM parameters โ€” defined in a single template.yaml. sam deploy --guided creates everything.
Data Safety
DeletionPolicy: Retain on Stateful Resources
DynamoDB tables and the S3 bucket have DeletionPolicy: Retain in the SAM template. Deleting the CloudFormation stack does not delete production data.