TinyLink is a production-quality, high-performance SaaS URL shortener built with a modern React + TypeScript frontend and a FastAPI (Python) backend, with Razorpay billing and Clerk authentication.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT BROWSER β
β React 19 + Vite Β· Apollo Client (GraphQL) Β· Clerk SDK (Auth) β
β Razorpay Checkout JS Β· Recharts Β· TailwindCSS v4 β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββββ
β HTTPS
ββββββββββββββββββΌβββββββββββββββββββ
β Vercel (CDN/Edge) β
β Static SPA β frontend/dist β
ββββββββββββββββββ¬βββββββββββββββββββ
β API calls
ββββββββββββββββββΌβββββββββββββββββββ
β Render.com β FastAPI β
β uvicorn app.main:app β
β β
β ββββββββββββ βββββββββββββββββ β
β β /graphql β β REST routes β β
β βStrawberryβ β /{short_code} β β
β β GraphQL β β /api/payment/* β β
β ββββββ¬ββββββ β /api/webhooks/ β β
β β βββββββββ¬βββββββββ β
β ββββββΌβββββββββββββββΌβββββββββ β
β β SQLAlchemy ORM Layer β β
β ββββββ¬βββββββββββββ¬βββββββββββ β
βββββββββΌβββββββββββββΌββββββββββββββ
β β
βββββββββββββββΌβββ ββββββΌβββββββββββββββββ
β Supabase β β Upstash Redis REST β
β PostgreSQL DB β β (Cache + RateLimit) β
ββββββββββββββββββ βββββββββββββββββββββββ
External Services:
Clerk β JWT auth + user webhooks
Razorpay β payment order creation + HMAC verification
Brevo β transactional emails (plan upgrade / link created)
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite 8, TypeScript, Apollo Client v4, Clerk React, TailwindCSS v4, Framer Motion, Recharts |
| Backend | FastAPI, Strawberry GraphQL, SQLAlchemy 2, Uvicorn, python-jose |
| Auth | Clerk (JWKS-based JWT verification on every request) |
| Payments | Razorpay REST API β order creation + HMAC-SHA256 signature verification |
| Database | PostgreSQL via Supabase (connection pooling via PgBouncer port 6543) |
| Cache / Rate-limit | Upstash Redis REST API |
| Brevo (Sendinblue) transactional API | |
| Frontend Hosting | Vercel (static SPA + vercel.json SPA rewrites) |
| Backend Hosting | Render.com (web service) |
All operations require Authorization: Bearer <ClerkJWT> header.
Strawberry automatically converts Python snake_case field names to GraphQL camelCase.
| Query | Arguments | Returns | Description |
|---|---|---|---|
me |
β | UserType |
Authenticated user profile + current subscription plan |
myUrls |
page, limit, search, status, orderBy |
PaginatedURLsType |
Paginated user link list with filtering |
url |
id: UUID! |
ShortURLType |
Single link by ID (owner only) |
urlByCode |
shortCode: String! |
ShortURLType |
Metadata by short code (no click tracking) |
expiredUrls |
β | [ShortURLType] |
All expired links for current user |
favoriteUrls |
β | [ShortURLType] |
All favourited links |
analytics |
urlId: UUID!, days: Int |
AnalyticsType |
Click analytics (daily, browser, device, country, referrer) |
dashboardStats |
β | DashboardType |
Aggregate stats for dashboard |
| Mutation | Arguments | Returns | Description |
|---|---|---|---|
createShortUrl |
input: CreateShortURLInput! |
ShortURLType |
Create a new short link |
updateShortUrl |
id: UUID!, input: UpdateShortURLInput! |
ShortURLType |
Update link properties |
deleteShortUrl |
id: UUID! |
ShortURLType |
Soft-delete a link |
restoreShortUrl |
id: UUID! |
ShortURLType |
Restore a soft-deleted link |
toggleFavorite |
id: UUID! |
ShortURLType |
Star / unstar a link |
generateQrCode |
id: UUID! |
String |
Base64 PNG QR code data URI |
Input types (camelCase in GraphQL):
input CreateShortURLInput {
originalUrl: String!
customAlias: String
expiresAt: DateTime
title: String
}
input UpdateShortURLInput {
customAlias: String
expiresAt: DateTime
isActive: Boolean
title: String
}| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/{short_code} |
None | Redirect β checks rate limit β Redis cache β DB β 301 |
GET |
/health |
None | Health probe |
POST |
/api/webhooks/clerk |
SVIX signature | Clerk user sync webhook |
POST |
/api/payment/order |
Bearer JWT | Create Razorpay order for a plan |
POST |
/api/payment/verify |
Bearer JWT | Verify HMAC signature + upgrade user plan |
GET |
/api/payment/plans |
None | Return plan catalogue (prices, features) |
# Database
DATABASE_URL=postgresql://user:pass@host:6543/postgres
# Clerk
CLERK_PUBLISHABLE_KEY=pk_test_xxxx
# Redis (Upstash)
UPSTASH_REDIS_REST_URL=https://xxxx.upstash.io
UPSTASH_REDIS_REST_TOKEN=xxxx
# CORS & URLs
CORS_ALLOWED_ORIGINS=https://your-frontend.vercel.app
SHORT_URL_BASE=https://your-backend.onrender.com
# Razorpay β REQUIRED for payments. Set these on Render's environment tab.
RAZORPAY_KEY_ID=rzp_live_xxxx # or rzp_test_xxxx for test mode
RAZORPAY_KEY_SECRET=xxxx
# Brevo (Email)
BREVO_API_KEY=xxxx
BREVO_SENDER_EMAIL=you@example.com
BREVO_SENDER_NAME=TinyLink
β οΈ Razorpay 401 / 502 on production? TheRazorpay error 401: Authentication failedmeansRAZORPAY_KEY_IDand/orRAZORPAY_KEY_SECRETare not set (or wrong) in the Render service's Environment tab. Go to Render Dashboard β your service β Environment β Add env vars and paste the keys. After saving, Render will redeploy automatically.
VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxx
# IMPORTANT: VITE_API_URL must be the BARE base URL β NO trailing /graphql suffix.
# Wrong β: https://tinylink-57eg.onrender.com/graphql
# Correct β
: https://tinylink-57eg.onrender.com
VITE_API_URL=https://tinylink-57eg.onrender.com
# Full GraphQL endpoint (= VITE_API_URL + /graphql)
VITE_GRAPHQL_URL=https://tinylink-57eg.onrender.com/graphql
VITE_SHORT_URL_BASE=https://tinylink-57eg.onrender.com
β οΈ /graphql/api/payment/order404? This happens whenVITE_API_URLis accidentally set to the/graphqlendpoint. SetVITE_API_URLto the bare backend origin (no path). The code defensively strips any trailing/graphqlsuffix but the environment variable should be correct.
β οΈ Vercel env vars: Set the above in Vercel Dashboard β Project β Settings β Environment Variables.
cd backend
python -m venv .venv
# Windows:
.venv\Scripts\activate
# macOS/Linux:
source .venv/bin/activate
pip install -r requirements.txt
alembic upgrade head
uvicorn app.main:app --reload --port 8000cd frontend
npm install
npm run dev # dev server at http://localhost:5173
npm run build # production build β dist/- Connect your GitHub repo to Render as a Web Service
- Set Root Directory to
backend - Set Build Command:
pip install -r requirements.txt - Set Start Command:
uvicorn app.main:app --host 0.0.0.0 --port $PORT - Add all environment variables from the backend table above in Environment β Add env vars
- Connect your GitHub repo to Vercel
- Set Root Directory to
frontend - Add all
VITE_*environment variables in Project Settings β Environment Variables - The
vercel.jsoninfrontend/handles SPA routing rewrites automatically
| Plan | Price | Links | Analytics | Features |
|---|---|---|---|---|
| Free | βΉ0/mo | 25 | 7-day history | Basic QR codes |
| Pro | βΉ499/mo | 500 | 90-day history | Custom aliases, Link expiry, Priority support |
| Enterprise | βΉ1,999/mo | Unlimited | Unlimited | Custom domains, Bulk import/export, Dedicated support, SLA |
Plans auto-expire after 30 days and downgrade to Free if not renewed.