Alshorty V3 — Master Reference 👋
Your complete command centre. Tick tasks as you complete them. Click Save Progress often. Use Backup / Restore before clearing browser cache.
🧭 How to use this document (read this first)
This is your single source of truth for Alshorty V3 in production. Every section has tasks you can tick, steps you can expand, and warnings that protect you from trouble.
1
Navigate using the left sidebar — each section has a specific focus (Security, Legal, AdSense, Roadmap etc.)
2
Click any task item to mark it done (turns green ✓). Click again to unmark.
3
Click "▼ Show Steps" on any task — reveals a detailed step-by-step guide. Every task has one.
4
Press "💾 Save Progress" after completing tasks. Saves to your browser. Won't disappear unless you clear cache.
5
Use "⬇ Backup / Restore" to export your progress as JSON. Paste it into a Google Doc or Notion. Do this weekly.
6
Start with "🔥 Do Today" in the sidebar — your most urgent 10 items right now.
V3 is live in production! Magic link auth, Google OAuth, URL shortening, admin panel, Razorpay order creation, and link redirects are all working. The foundation is solid — now it's time to polish and grow.
Security Score
A−
Strong — 3 gaps remain
Legal Status
✓
GDPR + DPDP + IT Rules
AdSense Ready
65%
Cookie consent missing
Open Items
—
Across all sections
Loading...0%
🔴 Critical — Fix Now Blocking
Cookie consent banner before AdSense
Sitemap: regenerate to include all 15 blog posts
ads.txt: replace placeholder with real publisher ID
Confirm all 6 Worker secrets set (not in wrangler.toml)
Disabled user accounts must cascade to link deactivation
🟠 High Priority This Week
Create OG image (1200×630) — currently missing
Add blog cover images to /blog-images/
Add GSC verification code to index.html
Set up uptime monitoring (alshorty.com + api.alshorty.com)
Handle payment.refunded webhook — downgrade user
🔥 Do Today
Your 10 most impactful tasks right now. In order of urgency. Knock these out first.
Before you do anything else: Run
grep FRONTEND_ORIGIN worker/wrangler.toml — it must say https://alshorty.com. This keeps getting accidentally reset to dev3 and breaks magic links and short URL generation.TODAY'S PRIORITY LIST
1. Build cookie consent banner (GDPR + AdSense blocker)
📋 Step-by-step
- 1Create
ui/src/components/shared/CookieConsent.tsx. On first visit, checklocalStorage.getItem('alshorty_cookie_consent'). If null, show the banner. - 2Banner needs two buttons: "Accept all" (sets value to 'accepted') and "Reject non-essential" (sets value to 'rejected'). Store in localStorage.
- 3In
ui/index.html, the AdSense<script>tag is commented out. Conditionally inject it via JS only if consent === 'accepted'. Usedocument.createElement('script')(same pattern as Razorpay). - 4Add
<CookieConsent />toui/src/App.tsxat the root level — renders on every page before any ad loads. - 5Add a "Manage cookie preferences" link to the footer that re-opens the banner (delete the localStorage key, reload).
✅ This alone unblocks AdSense activation. A minimal self-built banner is sufficient — no paid CMP needed at your scale.
2. Regenerate sitemap.xml to include blog posts
📋 Step-by-step
- 1Open
generate-sitemap.mjsin project root. Ensure it reads all slugs fromui/src/lib/blog-data.tsand writes/blog/{slug}entries. - 2Run:
node generate-sitemap.mjsfrom project root. This rewritesui/public/sitemap.xml. - 3Verify the output file includes all 15 blog article URLs + tools pages + pricing + about.
- 4Add sitemap generation to your GitHub Actions
deploy-ui.yml— runnode generate-sitemap.mjsas a build step so it never goes stale again. - 5After deploying, submit the updated sitemap in Google Search Console → Sitemaps.
3. Fix ads.txt — replace placeholder publisher ID
📋 Step-by-step
- 1Go to AdSense dashboard → Account → Account information → copy your Publisher ID (format:
ca-pub-XXXXXXXXXXXXXXXX). - 2Open
ui/public/ads.txt. Replace the commented-out line with:google.com, ca-pub-YOURPUBID, DIRECT, f08c47fec0942fa0 - 3Deploy the UI. Then verify at
https://alshorty.com/ads.txt— you should see the plain text line.
4. Create OG image and add to ui/public/
📋 Step-by-step
- 1Open Canva.com → Create design → Custom size → 1200×630px.
- 2Add: your logo, tagline (e.g. "Shorten, track, and manage your links"), brand colours.
- 3Export as PNG. Save as
og-image.pnginui/public/. - 4Test at
developers.facebook.com/tools/debug/— paste alshorty.com and verify the preview shows your image.
5. Set up Cloudflare Worker error alerting
📋 Step-by-step
- 1Cloudflare Dashboard → Workers & Pages → alshorty-v3 → Observability → Notifications.
- 2Create alert: "Worker Error Rate" → trigger when >1% over 5 minutes → email your address.
- 3Also create: "Worker CPU Time" alert for >50ms average — early signal of performance regressions.
6. Set up uptime monitoring for both domains
📋 Step-by-step
- 1Sign up at betteruptime.com (free tier) or uptimerobot.com.
- 2Add monitor 1:
https://alshorty.com/health→ expect HTTP 200 → check every 3 minutes. - 3Add monitor 2:
https://api.alshorty.com/api/status→ expect HTTP 200 → check every 3 minutes. - 4Set alert to email + SMS/WhatsApp if available. You need to know within 5 minutes of downtime.
7. Add GSC verification code to index.html
📋 Step-by-step
- 1Go to
search.google.com/search-console→ Add property → URL prefix →https://alshorty.com. - 2Choose "HTML tag" verification → copy the
content="..."value. - 3In
ui/index.html, find the commented-out GSC meta tag and uncomment it, replacingYOUR_GSC_CODE_HEREwith your real code. - 4Deploy UI → click "Verify" in GSC → then submit sitemap at
https://alshorty.com/sitemap.xml.
8. Add blog cover images to ui/public/blog-images/
📋 Step-by-step
- 1Create folder:
ui/public/blog-images/ - 2Source royalty-free images from unsplash.com or pexels.com — one per article topic (URLs, QR codes, analytics, affiliate marketing, tech).
- 3Optimise each to WebP format at 1200×630px. Target under 80KB each. Use squoosh.app.
- 4Name each image to match the slugs referenced in
ui/src/lib/blog-data.ts.
9. Handle payment.refunded webhook in payments.js
📋 Step-by-step
- 1In
worker/src/routes/payments.js, in the webhook handler switch/if block, add a case forpayment.refunded. - 2Extract
emailfromevent.payload.payment.entity.notes.email. - 3Update D1:
UPDATE users SET plan='free', sub_expires_at=NULL WHERE email=? - 4Log the refund event:
INSERT INTO audit_log (email, action, meta, created_at)for traceability. - 5Optionally send a downgrade email via Resend so the user knows their plan changed.
10. Compress logo1.png (173KB→<20KB) and favicon.png (108KB→<5KB)
📋 Step-by-step
- 1Go to squoosh.app. Upload
logo1.png. Select WebP format. Reduce quality to 80. Target under 20KB. Save aslogo1.webp. - 2Upload
favicon.png. Resize to 64×64 or 32×32. Compress as PNG. Target under 5KB. Save back asfavicon.png. - 3Update any
<img src="logo1.png">references in the UI tologo1.webp. Usegrep -r "logo1.png" ui/src/to find all references. - 4Run PageSpeed Insights at
pagespeed.web.devon alshorty.com. Target 85+ on mobile before applying for AdSense.
🔐 Auth & Sessions
Magic link + Google OAuth. Sessions are 7-day HttpOnly Secure SameSite=None cookies on .alshorty.com. All in routes/auth.js.
V3 auth is solid. Magic link tokens are one-time-use SHA-256 hashed with 15-min expiry. Google OAuth validates the
aud claim against your Client ID. API keys are SHA-256 hashed, PRO-only, 5-key limit. Two open items below.COMPLETED ✓
✓
Magic link tokens: one-time-use, SHA-256 hashed, 15-min TTL in KV
✓
Session cookies: HttpOnly + Secure + SameSite=None + Domain=.alshorty.com
✓
Magic link rate limited: 5 per email per 15 minutes (KV counter, 900s TTL)
✓
Google OAuth validates aud claim against GOOGLE_CLIENT_ID
✓
API keys hashed (SHA-256), sk_ prefix, 5-key limit per user, PRO-only
✓
Admin secret stored in sessionStorage (clears on tab close, not persistent)
OPEN ITEMS
Disabled user accounts: existing links still redirect (should return 410)
📋 Fix: Cascade disable to links
- 1In
routes/redirect.js, after resolving the link, check the owner's account status:const user = await getUserByEmail(link.owner, env); - 2If
user?.is_disabled, returnhtmlPage(renderErrorPage('This link is no longer available', code, env), 410) - 3Alternative (better): In the admin disable handler in
admin-backend.js, when disabling a user, batch-update all their KV links tois_active: false. This avoids a DB lookup on every redirect.
⚠️ Until this is fixed, a banned abuser's phishing links remain 100% functional. This is your highest-priority security gap.
Add CSRF / Origin check on all state-mutating endpoints (POST/PUT/DELETE)
📋 Fix: Origin assertion
- 1In
utils/cors.js, export a helper:export function assertOrigin(request) { const o = request.headers.get('Origin'); return ALLOWED_ORIGINS.includes(o); } - 2At the top of
deleteLink,editLink,toggleLink,createLink,updateBiohandlers:if (!assertOrigin(request)) return forbidden('CSRF_CHECK_FAILED');
💡 This adds ~2 lines per handler. Low effort, meaningful protection against cross-origin form attacks.
🛡️ Input Validation
Multi-layer URL validation: validateUrl() + deepUrlScan() + PHISHING_PATTERNS regex. Expanded in April 2026.
Block lists expanded in April 2026: 40+ shortener domains, 25+ BLOCKED_DOMAINS (IP grabbers, adult content, paste sites), 35+ phishing regex patterns. URL input auto-prefixes https:// on paste and blur. PRO-only features enforced server-side.
COMPLETED ✓
✓
Multi-layer validation: scheme → private IP → shortener → TLD → BLOCKED_DOMAINS → keywords → phishing regex
✓
editLink and updateBio both validate URLs on every update (was missing in V2)
✓
Destination URL re-validated at redirect time (defense in depth)
✓
SSRF protection: validateProxyUrl() blocks private IPs/metadata endpoints on health-check and OG-checker
✓
URL input: auto-prefixes https:// on paste AND on blur. type="url" enforced.
✓
PRO-only features enforced server-side: expiry, max-clicks, password, UTM params, category prefix
OPEN ITEMS
Integrate Google Safe Browsing API — catches freshly-registered phishing domains
📋 Step-by-step
- 1Get a free Safe Browsing API key from console.cloud.google.com → APIs & Services → Safe Browsing API.
- 2Add as Worker secret:
wrangler secret put GOOGLE_SAFE_BROWSING_KEY - 3In
createLink(), afterdeepUrlScan()passes, call:POST https://safebrowsing.googleapis.com/v4/threatMatches:findwith the URL. - 4If response contains matches, return
badRequest('This URL has been flagged as unsafe', 'BLOCKED_URL'). - 5Wrap in try/catch with fail-open behaviour — if GSB API is down, log the error and allow through. Don't let GSB downtime block your users.
💡 GSB free tier: 10,000 calls/day. At your scale you're safe for months.
UTM parameter length cap (200 chars) and control-char strip
📋 Quick Fix
- 1In
createLink(), add one line before appending UTM params:const trimUtm = v => v ? String(v).slice(0, 200).replace(/[\x00-\x1f]/g, '') : undefined; - 2Apply:
utm_source: trimUtm(utm_source)etc. for all 5 UTM fields.
🔒 Headers & CORS
Worker applies SECURITY_HEADERS via wrap() on every response. Cloudflare Pages gets headers via ui/public/_headers.
CORS restricted to explicit origin allowlist — no wildcards. Origin reflection required for credentials:include. All fixed in April 2026 patch bundle.
COMPLETED ✓
✓
Worker security headers on every response via wrap() — X-Frame-Options, nosniff, Referrer-Policy
✓
Cloudflare Pages _headers file with full CSP (script-src, style-src, connect-src, frame-src)
✓
CORS: explicit origin allowlist, no wildcard, Vary:Origin set, origin reflection for credentials
✓
FRONTEND_ORIGIN = https://alshorty.com in wrangler.toml (was dev3.alshorty.com)
✓
_redirects: static files (sitemap.xml, robots.txt, ads.txt) served directly before SPA catch-all
OPEN ITEMS
Add HSTS header to Worker SECURITY_HEADERS
📋 Quick Fix
- 1In
worker/src/index.js, in theSECURITY_HEADERSobject, add:'Strict-Transport-Security': 'max-age=31536000; includeSubDomains' - 2In Cloudflare Dashboard → SSL/TLS → Edge Certificates → HSTS → Enable with max-age 1 year, includeSubDomains checked.
💡 HSTS tells browsers to always use HTTPS for your domain — even if the user types http://. Also enables HSTS Preload eligibility for maximum protection.
Remove dev3 Worker routes once prod is confirmed stable for 2+ weeks
📋 Clean-up steps
- 1Wait until prod has been stable for 2+ weeks — you may need dev3 for rollback.
- 2Cloudflare Dashboard → Workers → alshorty-v3 → Settings → Domains & Routes → delete dev3.alshorty.com/api* and dev3-api.alshorty.com/* routes.
- 3Create a new separate Worker called
alshorty-devand add the dev3 routes there.
💳 Payments & Razorpay
Razorpay integration is live and working. Two bugs were fixed in April 2026. Two items remain open.
Razorpay checkout is working. Order creation fixed (receipt was 44 chars, limit is 40). SDK now loaded correctly via DOM API in useEffect — not broken JSX script tag. Webhook HMAC verified. Replay protection active.
COMPLETED ✓
✓
Razorpay order creation: receipt ≤40 chars (was 44 — caused all 500 errors), currency always INR
✓
Razorpay SDK loaded via document.createElement('script') in useEffect — not JSX script tag
✓
Webhook HMAC-SHA256 verified before processing + replay protection via KV nonce (10-min TTL)
✓
subscription.cancelled and subscription.halted webhooks handled — downgrade to free immediately
OPEN ITEMS
Handle payment.refunded webhook — downgrade user plan when refund issued
📋 Step-by-step
- 1In
worker/src/routes/payments.js, webhook handler: add case forpayment.refundedevent. - 2Extract email from
event.payload.payment.entity.notes.email. - 3Update D1:
UPDATE users SET plan='free', sub_expires_at=NULL WHERE email=? - 4Log to audit trail:
INSERT INTO audit_log (email, action, meta, created_at). - 5Optionally send a downgrade notification email via Resend.
Poll for plan activation after payment (KV eventual consistency delay — up to 60s)
📋 Step-by-step
- 1After Razorpay payment success callback fires, redirect to
/billing?upgraded=1instead of refreshing in-place. - 2On the Billing page, detect
?upgraded=1in URL params. If present, start pollingapi.auth.session()every 2 seconds for up to 30 seconds. - 3While polling, show a spinner: "Activating your Pro plan… this takes a few seconds."
- 4Once
session.plan === 'pro', stop polling and show a success message. Remove?upgraded=1from URL.
💡 Most KV updates propagate in under 5 seconds. 30s polling covers even the slowest edge cases.
Set up Razorpay webhook failure monitoring — check delivery history weekly
📋 Monitoring setup
- 1Razorpay Dashboard → Settings → Webhooks → your endpoint → check "Recent Deliveries" weekly.
- 2Consider logging all received webhook events to D1 with timestamp and event type for a permanent audit trail.
- 3Add a worker health endpoint:
GET /__admin/billing/verify?email=xto manually check a user's plan status vs. what Razorpay says.
🗝️ Secrets & Config
All secrets must be in Cloudflare's encrypted secrets store — never in wrangler.toml which is in git.
Critical rule: wrangler.toml is committed to git. Any value in [vars] is visible to anyone with repo access. ALL sensitive values must be set via
wrangler secret put and referenced as env.SECRET_NAME — never hardcoded in the file.COMPLETED ✓
✓
Dev routes (/__dev/*) return 404 in production — ENV=production guard in wrangler.toml
✓
GitHub Actions CI/CD uses secrets references — no hardcoded values in workflow YAML
✓
Admin secret in sessionStorage (clears on tab close) — not localStorage (persists forever)
REQUIRED ACTION
Verify all 6 production Worker secrets are set via wrangler secret put
📋 Required secrets
- 1
wrangler secret put ADMIN_SECRET— the secret header value for /__admin/* endpoints - 2
wrangler secret put RESEND_API_KEY— magic link emails will fail without this - 3
wrangler secret put RAZORPAY_KEY_ID— payment order creation - 4
wrangler secret put RAZORPAY_KEY_SECRET— payment order creation - 5
wrangler secret put RAZORPAY_WEBHOOK_SECRET— webhook HMAC verification - 6
wrangler secret put GOOGLE_CLIENT_ID— Google OAuth (and Microsoft when activated)
💡 To verify what's set:
wrangler secret list — shows secret names (not values) currently deployed.Add .wrangler/ and .dev.vars to .gitignore
📋 Quick Fix
- 1Add to
.gitignorein the repo root:.wrangler/,.dev.vars,*.sqlite,*.sqlite-shm,*.sqlite-wal - 2Check if already committed:
git ls-files .wrangler/. If output is non-empty, run:git rm -r --cached .wrangler/ && git commit -m "remove wrangler state from tracking"
Update compatibility_date in wrangler.toml from 2024-06-01 to current
📋 Safe upgrade path
- 1Update
worker/wrangler.toml:compatibility_date = "2025-04-01" - 2Test locally first:
wrangler dev --remote— some API behaviours changed between dates. - 3Deploy to dev3 first. Test full flow (auth, shorten, redirect, payments) before pushing to prod.
🏗️ Architecture
Cloudflare Worker + Pages + D1 + KV + Resend + Razorpay. Worker routes intercept short-link paths before Pages serves the SPA.
Stack: Worker handles api.alshorty.com/* (via CNAME) + specific paths on alshorty.com (via Worker Routes). Pages serves the React SPA for everything else. D1 for relational data (users, analytics, API keys). KV for links, sessions, rate limits, bio slug index.
COMPLETED ✓
✓
All 4 domain prefixes working: /link/ /s/ /go/ /run/ — backward-compatible with legacy bare-path links
✓
Cloudflare Worker routes configured: /api/*, /link/*, /s/*, /go/*, /run/*, /bio/*, /health, /auth/verify
✓
Bio slug index: O(1) KV lookup via bio_slug:{slug} — was O(n) full scan
✓
PRO feature gates: frontend (ProRoute, UI locks) + backend (UPGRADE_REQUIRED 403) two-layer enforcement
✓
PRO users get instant 302 redirect. Free/anon users get 5-second interstitial page.
✓
404 for dead short links: styled HTML page instead of bare JSON {"error":"NOT_FOUND"}
✓
Worker fully modularised: routes/, utils/, admin/, templates/, qr/, emails/, config/
OPEN ITEMS
Analytics writes on redirect hot path — move to ctx.waitUntil() background
📋 Fix: Background writes
- 1In
routes/redirect.js, find the analytics insert call inrecordClick(). - 2Wrap it:
ctx.waitUntil(recordClick(request, env, link, code, destination)) - 3Also wrap the KV click_count increment in waitUntil.
- 4Return the
Response.redirect(destination, 302)BEFORE any DB writes. User never waits for analytics.
✅ This change alone can reduce p95 redirect latency by 30-60ms at scale.
Remove V3Launch.tsx banner component post-stabilisation
Add unit test suite for validation, auth, and security helpers (vitest is configured)
📋 Test coverage priorities
- 1validateUrl(): test private IPs (10.x, 192.168.x), .tk/.cf TLDs, shortener domains, phishing keywords, javascript: scheme, empty string, null, very long strings.
- 2validateProxyUrl(): test 192.168.x.x, 10.x.x.x, 172.16-31.x.x, 127.0.0.1, ::1, 169.254.169.254, file://, ftp://.
- 3isValidEmail(): test co.in, com.au, ac.uk, gov.in multi-part TLDs — historically buggy.
- 4Webhook HMAC: test valid signature, tampered body, wrong secret, replay (duplicate nonce).
🚫 Abuse Prevention
Multi-layer: rate limiting, honeypot banning, IP hashing, expanded block lists, bot UA detection.
COMPLETED ✓
✓
Multi-layer rate limiting: 120/min global IP, 20/min auth endpoints, per-user plan quotas
✓
Honeypot auto-ban: 3 hits on scanner paths = 7-day IP ban (returns generic 404)
✓
IP hashing: IPs never stored raw — salted SHA-256 hash used for all rate limit keys
✓
Expanded block lists: 40+ shortener domains, 25+ BLOCKED_DOMAINS, 35+ phishing patterns
✓
Bot UA blocking on write endpoints (sqlmap, nikto, dirbuster, masscan) — read endpoints unaffected
OPEN ITEMS
Disabled user accounts: links still redirect — propagate disable to all user's links
📋 Fix approach
- 1Best approach: in
admin-backend.js, when disabling a user, run a KV list scan for all their links (prefix: 'link:', filter bylink.owner === email) and setis_active: falseon each. - 2This is a one-time background operation on the admin action — not on every redirect request. Use
ctx.waitUntil()to run it async. - 3For immediate effect: add an owner check in redirect.js as a faster intermediate fix (2 KV lookups per redirect — owner lookup + link lookup).
Google Safe Browsing API integration (also in Validation section)
⚖️ Legal & Compliance
GDPR + India DPDP Act 2023 + India IT Rules 2021 + Consumer Protection Act 2019. All patched in April 2026.
Legal pages are strong. GDPR Article 6 legal bases added. India DPDP Act 2023 section added. Grievance Officer added to Terms (India IT Rules 2021). DMCA jurisdiction corrected. Minimum age aligned across Terms and Privacy (13 global, 16 EU/UK).
COMPLETED ✓
✓
Privacy Policy: GDPR Article 6 legal bases, DPDP Act 2023, AdSense disclosure, data retention periods
✓
India DPDP Act 2023 rights section: access, correction, erasure, grievance, nomination
✓
Terms: Grievance Officer section added (India IT Rules 2021) — 24h ack, 15-day resolution SLA
✓
DMCA jurisdiction corrected: US Federal Court → courts of Mumbai, India
✓
Minimum age aligned: 13 globally, 16 in EU/UK — consistent across Terms and Privacy
✓
Footer sitemap link fixed: opens as <a href="/sitemap.xml"> not React Router Link (which caused 404)
OPEN ITEMS
Cookie consent banner — GDPR blocks AdSense without prior consent for EU/UK users
📋 What you need legally
⚠️ This is the most important legal item. Do not activate AdSense before this is in place.
- 1Banner must appear on first visit before any advertising cookies load.
- 2Must offer "Accept" and "Reject non-essential" as equally prominent options. No dark patterns (pre-ticked, buried reject).
- 3Must link to Cookie Policy page.
- 4Store choice in localStorage. Only inject AdSense script if consent = 'accepted'.
- 5Add "Manage cookie preferences" link in footer that clears consent and re-shows the banner.
💡 A self-built banner is sufficient for a small site. You don't need a paid CMP (OneTrust, Cookiebot) at your scale.
Refund policy: add specific process and timeline (India Consumer Protection Act 2019)
📋 What to add to Terms
- 1Add trigger criteria: refunds apply for technical failures or double-charges. Change-of-mind does not qualify after 24 hours.
- 2Add process: "Email hello@alshorty.com with subject 'Refund Request' and your Razorpay payment ID."
- 3Add timeline: "Refunds processed within 5–7 business days to original payment method."
💰 AdSense Readiness
Track everything needed for AdSense approval and successful activation. Do not apply until all Critical items are done.
Do not apply for AdSense yet. Cookie consent banner is missing (automatic rejection), blog cover images are broken, and sitemap is incomplete. Fix these three first. AdSense reviewers manually check your site.
COMPLETED ✓
✓
Privacy Policy mentions AdSense cookies (__gads, __gpi, IDE) and opt-out links
✓
All legal pages present and linked in footer: Privacy, Terms, DMCA, Cookie Policy, About, Contact, Sitemap
✓
15 original blog articles with headings, read time, categories — strong editorial depth
✓
Ad-bearing pages (blog, home, tools, redirect) accessible without login
✓
AdSlot component renders nothing until VITE_ADSENSE_CLIENT_ID is set — safe to ship
✓
HTTPS enforced on all origins via Cloudflare
REQUIRED BEFORE APPLYING
Cookie consent banner (GDPR — AdSense will auto-reject without this)
Blog cover images created and deployed (15 broken img tags visible to reviewers)
ads.txt: replace placeholder ca-pub-XXXXXXXXXXXXXXXX with real publisher ID
Sitemap regenerated to include all 15 blog posts and submitted to GSC
OG image created and deployed (1200×630px)
PageSpeed Insights score: 85+ on mobile before applying
Add AdSense ad slot to redirect interstitial page — primary monetisation window for URL shorteners
📋 Implementation + AdSense policy notes
⚠️ AdSense Better Ads Standards: "Continue" button must always be visible above fold, never visually adjacent to the ad unit. Max redirect delay: keep at 5 seconds or less.
- 1In
worker/src/templates/redirect-page.js, add an AdSense ad slot div between the destination info and the footer. Include the publisher ID from env. - 2Gate it: only render the ad slot when
link.owner_plan !== 'pro'. PRO users' links have no ads. - 3Keep the "Continue now →" button above the fold. Ensure it is never visually near the ad unit.
- 4Pre-reserve space for the ad (e.g. min-height: 250px) to prevent layout shift.
Verify alshorty.com domain age ≥ 6 months before applying
Set up Google Analytics 4 and link to AdSense after approval
📋 Setup steps
- 1Create GA4 property at analytics.google.com for alshorty.com.
- 2Add gtag.js to index.html — conditionally load with cookie consent (same pattern as AdSense script).
- 3After AdSense approval, link GA4 to AdSense in both platforms for enhanced reporting.
Activate AdSense: set VITE_ADSENSE_CLIENT_ID in Cloudflare Pages + uncomment script in index.html
📋 Activation steps
- 1In Cloudflare Pages Settings → Variables and Secrets: add
VITE_ADSENSE_CLIENT_ID = ca-pub-YOURPUBID - 2In
ui/index.html, uncomment the AdSense<script async src="https://pagead2.googlesyndication.com/...">tag. Replace placeholder publisher ID. - 3Add
VITE_ADSENSE_CLIENT_IDto GitHub Actionsdeploy-ui.ymlenv vars. - 4Deploy → ads render wherever AdSlot components exist. They render nothing if the env var is unset.
🔍 SEO & Crawlability
robots.txt, sitemap, structured data (JSON-LD), canonical URLs. Several items need action before organic traffic can grow.
COMPLETED ✓
✓
robots.txt: blocks all app/auth paths, allows blog/tools/pricing, references sitemap
✓
SEO meta tags, canonical URLs, OG tags, Twitter Card, JSON-LD structured data in index.html
✓
sitemap.xml works at /sitemap.xml — footer link fixed to open as <a> not React Router Link
OPEN ITEMS
Sitemap missing all 15 blog posts — regenerate with generate-sitemap.mjs
Add GSC verification code to index.html and submit sitemap
Compress logo1.png (173KB) and favicon.png (108KB) — Core Web Vitals impact
Add sitemap generation as a CI build step so it never goes stale
📋 CI integration
- 1In
.github/workflows/deploy-ui.yml, before thevite buildstep, add:- run: node generate-sitemap.mjs - 2The generated
ui/public/sitemap.xmlwill be included in the Vite build output and deployed automatically.
💡 This means you never have to remember to manually regenerate the sitemap when adding blog posts.
🗓️ Feature Roadmap
Prioritised by user value and monetisation impact. Wait 2–4 weeks of production stability before starting new features.
Rule of thumb: Don't start V3.1 features until V3 has been stable in production for 2 full weeks. This gives time to catch and fix real-world bugs without compounding new code on top of them.
🚀 V3.1 — Next 4 Weeks
- Cookie consent banner (GDPR — blocks AdSense)
- OG image created and deployed
- Blog cover images (15 articles)
- GSC verification + sitemap submitted
- GA4 set up and linked to AdSense pipeline
- Image assets compressed (logo + favicon)
- Razorpay: payment.refunded webhook handler
- ads.txt publisher ID filled in
- Disabled user → links 410 cascade fix
- HSTS header added to Worker responses
🔧 V3.2 — 1–2 Months
- CSRF Origin check on all mutating endpoints
- Google Safe Browsing API integration
- Analytics writes moved to ctx.waitUntil() background
- Payment plan activation poll (KV consistency UX)
- Ad slot added to redirect interstitial page
- UTM parameter length cap and control-char strip
- Unit test suite (validateUrl, webhook HMAC, etc.)
- Admin link pagination (cursor-based at scale)
- Razorpay webhook failure monitoring/alerting
✨ V4 Features — 2–4 Months
- Custom domain support for PRO users (links.yoursite.com)
- Team/workspace accounts (multi-user collaboration)
- Webhook delivery for click events (Zapier/n8n)
- Link rotation (round-robin across multiple destinations)
- Retargeting pixel management UI (FB, Google, TikTok)
- Folder/tag organisation for link management
- Scheduled link activation/deactivation
- API v2 with rate-limited public sandbox
- QR code bulk generation
🏛️ Infrastructure — 3–6 Months
- Cloudflare Durable Objects for atomic click counters
- D1 read replicas once Cloudflare GA
- Admin link audit log (who changed what and when)
- Automated billing emails (invoice on charge)
- E2E tests (Playwright): shorten→redirect, checkout flow
- OpenAPI spec auto-generated from Worker route handlers
- Multi-region KV pre-warming for viral link bursts
- Automated abuse report ingestion (GSB reporting)
🔧 Ops & Monitoring
Set up visibility before users start reporting problems. Free tools exist for everything here.
SET UP NOW
Cloudflare Worker error alerting: error rate >1% over 5 minutes → email alert
📋 Setup
- 1Cloudflare Dashboard → Workers & Pages → alshorty-v3 → Observability → Notifications.
- 2Create alert: "Worker Error Rate" → trigger when >1% over 5 minutes → your email.
- 3Create second alert: "Worker CPU Time" → trigger when >50ms average → performance regression signal.
Uptime monitoring for alshorty.com and api.alshorty.com — alert within 3 minutes of downtime
📋 Setup
- 1Sign up at betteruptime.com (free) or uptimerobot.com (free).
- 2Add:
https://alshorty.com/health→ expect 200 → check every 3 minutes. - 3Add:
https://api.alshorty.com/api/status→ expect 200 → check every 3 minutes. - 4Set alerts to email + SMS if available.
Enable Cloudflare D1 query analytics — identify slow queries before they cause timeouts
📋 What to check
- 1In D1 query analytics, filter for queries taking >50ms. These are your bottlenecks.
- 2Most common issue: unindexed
clickstable. Verify:CREATE INDEX IF NOT EXISTS idx_clicks_link_code ON clicks(link_code, created_at) - 3Also check
userstable has index onemail— it's queried on every authenticated request.
Weekly check: Razorpay webhook delivery history for failed events
Create separate alshorty-dev Worker for dev3 routes (separate from production Worker)
✅ Pre-Deploy Sanity Checklist
Run this before EVERY production deployment. Takes 5 minutes. Catches the most common issues.
This checklist resets. Unlike other panels, these items should be re-checked before every deploy. They are not "done forever" — they are "done for this deployment."
WORKER DEPLOY CHECKS
Verify FRONTEND_ORIGIN = https://alshorty.com in wrangler.toml (not dev3)
wrangler deploy --dry-run passes with no errors
No secrets accidentally in wrangler.toml [vars] section
UI DEPLOY CHECKS
TypeScript build passes: tsc --noEmit && vite build with no errors
sitemap.xml regenerated if any blog posts were added or removed
POST-DEPLOY SMOKE TESTS
Smoke test: shorten a URL → click it → verify 5-second redirect page appears
Check browser console: only expected noise (__admin/config 401 on Pricing page is normal)
Verify magic link email has correct domain (alshorty.com, not dev3)
alshorty.com/sitemap.xml returns valid XML (not SPA 404)
alshorty.com/ads.txt is accessible and contains valid content (not ## commented placeholder)
📝 Notes
Your private scratchpad. Saves automatically with your progress. Back up regularly.
🔑 Credentials & Keys (never store real values here — store hints only)
📌 Decisions & Context
🐛 Bug Log & Known Issues
📈 Scalability
Edge-native architecture on Cloudflare. KV for hot reads, D1 for relational data, Workers run globally at 200+ PoPs.
Current stack handles ~10 million redirects/month comfortably before any optimisation is needed. The biggest immediate risk is D1 writes on the redirect hot path — fix that first.
📊 Current Limits (Cloudflare Free/Paid)
| Resource | Free | Paid ($5/mo) |
|---|---|---|
| Worker requests/day | 100,000 | 10 million |
| Worker CPU time/request | 10ms | 50ms |
| KV reads/day | 100,000 | Unlimited |
| KV writes/day | 1,000 | Unlimited |
| D1 reads/day | 5 million | 25 billion |
| D1 writes/day | 100,000 | 50 million |
⚠️ Each redirect = 1 Worker req + 1 KV read + 1 D1 write (analytics). At 100k redirects/day you'll hit the free D1 write limit. Move analytics to waitUntil() first.
🚦 When to Scale
| Signal | Action |
|---|---|
| Worker CPU >30ms avg | Profile + optimise routes |
| D1 writes >80k/day | Upgrade to $5 Workers plan |
| KV reads >80k/day (free) | Upgrade or batch reads |
| D1 queries >50ms | Add indexes (see below) |
| >50k active links in KV | Consider prefix-sharding |
| Click counters race condition | Durable Objects (V4) |
IMMEDIATE PERFORMANCE WINS
Move analytics D1 writes to ctx.waitUntil() — removes ~30ms from every redirect
📋 Fix
- 1In
routes/redirect.js, find whererecordClick()is called. - 2Change from:
await recordClick(...)
To:ctx.waitUntil(recordClick(...)) - 3Return the redirect response immediately above this line — user gets the 302 before any DB work.
- 4Same for KV click_count increment — wrap in waitUntil.
✅ This single change reduces p95 redirect latency by 30-60ms and protects against D1 rate limits.
Verify D1 indexes exist on clicks(link_code, created_at) and users(email)
📋 Check and add indexes
- 1Check existing indexes:
wrangler d1 execute alshorty-db-prod --remote --command="SELECT name,tbl_name FROM sqlite_master WHERE type='index'" - 2Add if missing:
wrangler d1 execute alshorty-db-prod --remote --command="CREATE INDEX IF NOT EXISTS idx_clicks_link ON clicks(link_code, created_at DESC)" - 3
wrangler d1 execute alshorty-db-prod --remote --command="CREATE INDEX IF NOT EXISTS idx_clicks_time ON clicks(created_at DESC)" - 4
wrangler d1 execute alshorty-db-prod --remote --command="CREATE INDEX IF NOT EXISTS idx_users_email ON users(email)"
Upgrade to Cloudflare Workers Paid plan ($5/month) when approaching 80k requests/day
📋 How to upgrade
- 1Monitor daily requests: Cloudflare Dashboard → Workers → alshorty-v3 → Metrics → Requests.
- 2When approaching 80k/day, go to: Cloudflare → Workers → Plans → Upgrade to Workers Paid ($5/month).
- 3No code changes needed — the same Worker runs on the paid plan automatically.
💡 $5/month gives you 10M requests/day. At average 30-second sessions, that's ~3,000 concurrent users.
Implement click counter with Cloudflare Durable Objects for atomic increments (V4)
📋 When to do this
- 1This only matters for links getting 100+ simultaneous clicks (viral links). At your current scale, the race condition causes maybe 0.1% under-count. Acceptable.
- 2When you need exact counts: create a Durable Object
LinkCounterwith an atomicincrement()method. Each link gets its own DO instance keyed by link code. - 3Durable Objects are available on Workers Paid plan. No extra cost at your scale.
✓
Bio slug lookup: O(1) KV index (bio_slug:{slug}) — was O(n) full scan
✓
Multi-layer rate limiting: 120/min global IP, 20/min auth, per-user plan quotas
📅 Growth Timeline
Structured tasks organised by time horizon. Work through these in sequence — each phase builds on the last.
V3 is live. The 7-day tasks fix immediate revenue and legal blockers. 30 days gets AdSense approved. 3 months builds organic traffic. 6 months to first meaningful MRR milestone. 12 months positions you for a real product launch.
🔥 Days 1–7 Do This Week
Fix disabled user → links still redirect (security gap)
Build and deploy cookie consent banner
Regenerate sitemap.xml with all 15 blog posts + submit to GSC
Set up uptime monitoring (BetterUptime/UptimeRobot)
Set up Cloudflare Worker error alerting (error rate >1% → email)
Handle payment.refunded webhook — downgrade user on refund
Move D1 analytics writes to ctx.waitUntil() background
🟠 Days 8–30 AdSense Ready
Create and deploy OG image (1200×630px) + all 15 blog cover images
Compress logo1.png (173KB→<20KB WebP) and favicon.png (108KB→<5KB)
Fix ads.txt with real publisher ID + apply for Google AdSense
Add Google Analytics 4 (conditionally loaded with cookie consent)
Add AdSense ad slot to free-user redirect interstitial page
Write 5 new blog posts targeting high-value keywords (URL shortener comparison, etc.)
🟢 Months 1–3 Traffic & Revenue
Add CSRF Origin checks on all mutating endpoints (createLink, deleteLink, updateBio)
Integrate Google Safe Browsing API (free tier, 10k lookups/day)
Implement payment activation polling (KV eventual consistency UX)
Publish 10 more blog posts + start internal linking strategy
Set up weekly automated backup (cron + backup.sh)
🔵 Months 3–6 Feature Expansion
Custom domain support for PRO users (links.yoursite.com)
Webhook delivery for click events (Zapier/Make/n8n integration)
Team/workspace accounts (multi-user collaboration)
Folder/tag organisation + link rotation + scheduled activation
⭐ Months 6–12 Scale & Monetise
Durable Objects for atomic click counters (eliminate race condition at scale)
API v2 with public sandbox, rate-limited free tier, OpenAPI spec
E2E test suite (Playwright): shorten → redirect, checkout, auth flows
Multi-region KV pre-warming for viral link bursts
⚙️ Config & Ops Guide
Your complete operational manual. How the code is structured, how things connect, every command you need, and how to do every routine task. Bookmark this page.
🏗️ How Everything Connects
USER REQUEST FLOW
Browser → alshorty.com (Cloudflare Pages SPA)
Browser → alshorty.com/link/* → Worker → KV lookup → redirect page HTML
Browser → alshorty.com/api/* → Worker → D1/KV → JSON response
Browser → api.alshorty.com/* → Worker (via CNAME) → D1/KV → JSON
WORKER INTERNAL ROUTING (index.js)
Request → security checks → rate limit → route to handler:
/api/auth/* → routes/auth.js
/api/shorten → routes/links.js
/api/links → routes/links.js
/api/bio → routes/links.js
/api/analytics → routes/analytics.js
/api/payments/* → routes/payments.js
/api/webhooks/* → routes/payments.js
/__admin/* → admin/admin-backend.js
/link/* /s/* /go/* /run/* /bio/* → routes/redirect.js
SECURITY LAYER (applied to every request)
security.js → bot UA check, global rate limit (120/min), IP ban check, honeypot
constants.js → SOURCE OF TRUTH for all limits, block lists, pricing
validation.js→ URL validation, alias validation, domain type checks
cors.js → CORS headers, origin allowlist
📁 Worker File Structure
worker/
wrangler.toml ← config, KV/D1 bindings, routes
package.json
src/
index.js ← ROUTER + kill switch
config/
constants.js ← ALL limits & block lists ← EDIT HERE
routes/
auth.js ← magic link, Google, Microsoft
links.js ← create/edit/delete links, bio
redirect.js ← handles /link/, /s/, /go/, /run/
analytics.js ← click stats, export
payments.js ← Razorpay order + webhook
utils/
security.js ← rate limiting, bot detection
validation.js ← URL + alias validation
auth.js ← session cookies, token utils
cors.js ← CORS headers, origin check
response.js ← ok(), badRequest(), notFound()
admin/
admin-backend.js ← all /__admin/* endpoints
templates/
redirect-page.js ← 5-second interstitial HTML
bio-page.js ← /bio/{slug} HTML
db/
schema.sql ← D1 table definitions
📁 UI File Structure
ui/
index.html ← meta tags, GSC, AdSense script
vite.config.ts
public/
_headers ← Cloudflare Pages security headers
_redirects ← SPA catch-all (/* /index.html)
sitemap.xml ← auto-generated, do not edit manually
robots.txt
ads.txt ← AdSense publisher ID goes here
src/
App.tsx ← all routes defined here
lib/
api.ts ← ALL API calls from frontend
utils.ts ← formatNumber, formatDate, etc.
pages/
Dashboard.tsx, Analytics.tsx
Pricing.tsx, Billing.tsx
Admin.tsx ← admin panel
LinkInBio.tsx, Bulk.tsx
Privacy.tsx, Terms.tsx, DMCA.tsx
components/
shared/ShortenForm.tsx ← main URL shortening form
shared/ProRoute.tsx ← PRO plan route guard
layout/Header.tsx
layout/Footer.tsx
contexts/
AuthContext.tsx ← session state, user object
🔑 Key Variables — Where to Change What
| What you want to change | File | Variable/Section | Notes |
|---|---|---|---|
| Pricing (₹ amounts) | constants.js | PRICING.MONTHLY.IN | Or Admin → Config panel (no redeploy) |
| Free plan limits | constants.js | LIMITS.FREE | links_per_day, links_per_month |
| PRO plan limits | constants.js | LIMITS.PRO | links_per_day, links_per_month |
| Anon user link limit | constants.js | ANON_MAX_URLS | Default: 2 links per 24h per IP |
| Blocked shortener domains | constants.js | BLOCKED_SHORTENER_DOMAINS | Add domains to block re-shortening |
| Blocked domains (adult/malware) | constants.js | BLOCKED_DOMAINS | IP grabbers, paste sites, adult content |
| Blocked TLDs | constants.js | BLOCKED_TLDS | .tk .ml .cf .ga .gq etc. |
| Phishing keywords | constants.js | SUSPICIOUS_KEYWORDS | Crypto scams, credential phishing |
| Phishing regex patterns | security.js | PHISHING_PATTERNS | Complex pattern matching |
| Global rate limit | security.js | _checkGlobalRate() | 120 req/min general, 20 req/min auth |
| Allowed CORS origins | constants.js | ALLOWED_ORIGINS | Add new domains here if adding subdomains |
| Production domain | wrangler.toml | FRONTEND_ORIGIN | Must be https://alshorty.com — check before every deploy |
| Free link expiry time | constants.js | FREE_LINK_EXPIRY_MS | How long anon links survive |
| Bot UA block list | security.js | BOT_UA_FRAGMENTS | Scanner/exploit tool user agents |
| API base URL (frontend) | CF Pages env vars | VITE_API_URL | https://api.alshorty.com |
| AdSense publisher ID | CF Pages env vars | VITE_ADSENSE_CLIENT_ID | Set after AdSense approval |
🚀 Deployment Commands — Every Scenario
WORKER DEPLOY
# Standard deploy (most common — do this after any worker code change)
cd worker
wrangler deploy
# Dry run first (always safe to do before a real deploy)
wrangler deploy --dry-run
# Watch live logs after deploying (tail the production worker)
wrangler tail
# Filter logs to just errors
wrangler tail --format pretty --status error
# Local dev against REAL production data (careful — uses prod KV/D1)
wrangler dev --remote
# Check what's deployed right now
wrangler deployments list
# Rollback to previous version
wrangler rollback
FRONTEND DEPLOY
# Normal deploy — just push to main and GitHub Actions deploys automatically
cd ui
git add -A
git commit -m "your message"
git push origin main
# GitHub Actions runs deploy-ui.yml → Cloudflare Pages builds + deploys
# Build locally to test before pushing
npm run build
npx serve dist # preview at localhost:3000
# TypeScript check (catches errors before push)
npx tsc --noEmit
# Force redeploy without code changes (via Cloudflare dashboard)
# → Pages → alshorty-v3-frontend → Deployments → Retry deployment
SITEMAP REGENERATE
# Run from project root (not from ui/ or worker/)
node generate-sitemap.mjs
# This writes to ui/public/sitemap.xml — then commit and push
git add ui/public/sitemap.xml
git commit -m "regenerate sitemap"
git push
🗄️ Database Operations (D1)
# ── READ DATA ────────────────────────────────────────────────
# Count total users
wrangler d1 execute alshorty-db-prod --remote --command="SELECT COUNT(*) as total FROM users"
# List latest 10 users with plan
wrangler d1 execute alshorty-db-prod --remote --command="SELECT email, plan, created_at FROM users ORDER BY created_at DESC LIMIT 10"
# Count clicks today
wrangler d1 execute alshorty-db-prod --remote --command="SELECT COUNT(*) as clicks_today FROM clicks WHERE date(created_at/1000,'unixepoch')=date('now')"
# List all PRO users
wrangler d1 execute alshorty-db-prod --remote --command="SELECT email, created_at FROM users WHERE plan='pro' ORDER BY created_at DESC"
# Check specific user
wrangler d1 execute alshorty-db-prod --remote --command="SELECT * FROM users WHERE email='user@example.com'"
# ── UPDATE DATA ──────────────────────────────────────────────
# Manually upgrade user to PRO
wrangler d1 execute alshorty-db-prod --remote --command="UPDATE users SET plan='pro' WHERE email='user@example.com'"
# Downgrade user to free
wrangler d1 execute alshorty-db-prod --remote --command="UPDATE users SET plan='free' WHERE email='user@example.com'"
# Make user an admin
wrangler d1 execute alshorty-db-prod --remote --command="UPDATE users SET is_admin=1 WHERE email='your@email.com'"
# Disable user account
wrangler d1 execute alshorty-db-prod --remote --command="UPDATE users SET is_disabled=1 WHERE email='bad@actor.com'"
# ── SCHEMA ───────────────────────────────────────────────────
# List all tables
wrangler d1 execute alshorty-db-prod --remote --command="SELECT name FROM sqlite_master WHERE type='table'"
# Apply schema (fresh install only)
wrangler d1 execute alshorty-db-prod --remote --file=worker/src/db/schema.sql
# Export full DB backup
wrangler d1 export alshorty-db-prod --remote --output=backup-$(date +%Y%m%d).sql
🗃️ KV Operations
# List KV namespaces
wrangler kv namespace list
# Count all link keys in SHORTY_LINKS namespace
wrangler kv key list --namespace-id <SHORTY_LINKS_ID> | python3 -c "import json,sys; print(len(json.load(sys.stdin)), 'keys')"
# Read a specific link
wrangler kv key get "link:abc123" --namespace-id <SHORTY_LINKS_ID>
# Delete a specific link (by code)
wrangler kv key delete "link:abc123" --namespace-id <SHORTY_LINKS_ID>
# List all session keys (rate limit keys)
wrangler kv key list --namespace-id <SHORTY_LIMITS_ID> --prefix "ratelimit:"
# Clear an IP ban manually
wrangler kv key delete "ban:<IP_HASH>" --namespace-id <SHORTY_LIMITS_ID>
# KV Namespace IDs (from wrangler.toml):
# SHORTY_LINKS ← links, bio pages, bio_slug index
# SHORTY_SESSIONS ← active session tokens
# SHORTY_LIMITS ← rate limits, IP bans, anon counters
# SHORTY_CACHE ← temporary cache
# SHORTY_QUEUE ← background job queue
🔐 Secrets Management
# List all secret names (never shows values)
wrangler secret list
# Add or update a secret
wrangler secret put RAZORPAY_KEY_ID
# (you'll be prompted to type the value — it's hidden)
# Delete a secret
wrangler secret delete SECRET_NAME
# Required secrets — all 8 must be set:
ADMIN_SECRET # /__admin/* access (generate with: openssl rand -hex 32)
RESEND_API_KEY # magic link emails (from resend.com)
RAZORPAY_KEY_ID # rzp_live_xxx (public key)
RAZORPAY_KEY_SECRET # rzp secret (NEVER expose in frontend)
RAZORPAY_WEBHOOK_SECRET # webhook HMAC verification
GOOGLE_CLIENT_ID # xxx.apps.googleusercontent.com
MICROSOFT_CLIENT_ID # Azure app client ID (optional)
MICROSOFT_CLIENT_SECRET # Azure app secret (optional)
🌐 Cloudflare Pages Environment Variables
Set these at: Cloudflare Dashboard → Workers & Pages → alshorty-v3-frontend → Settings → Variables and Secrets → Production. After adding/changing, trigger a new deployment.
| Variable | Value | Notes |
|---|---|---|
VITE_API_URL | https://api.alshorty.com | Worker API endpoint |
VITE_GOOGLE_CLIENT_ID | Secret — your Google OAuth client ID | Required for Google sign-in button |
VITE_RAZORPAY_KEY_ID | rzp_live_xxx | Public key — safe in frontend |
VITE_MICROSOFT_CLIENT_ID | Azure app client ID (optional) | Leave empty if not using Microsoft auth |
VITE_ADSENSE_CLIENT_ID | ca-pub-YOURPUBID | Set AFTER AdSense approval. Leave blank until then. |
PUBLIC_API_BASE_URL | https://api.alshorty.com | Used by some components directly |
🧹 Routine Housekeeping
WEEKLY (every Monday ~15 min)
Check Razorpay webhook delivery history for failed events
Review Cloudflare Worker error rate + CPU time in dashboard
Run database backup: wrangler d1 export alshorty-db-prod --remote
📋 Full backup procedure
- 1
cd worker && wrangler d1 export alshorty-db-prod --remote --output=backups/db-$(date +%Y%m%d).sql - 2Also export KV keys:
wrangler kv key list --namespace-id <SHORTY_LINKS_ID> > backups/kv-keys-$(date +%Y%m%d).json - 3Upload both files to Google Drive or Dropbox for offsite storage.
- 4Keep last 4 weeks of backups, then delete older ones.
⚠️ Cloudflare does NOT back up your D1 data automatically. If you delete the DB or corrupt it, only your manual backup can save you.
MONTHLY (first of each month ~30 min)
Review admin panel: users, links, payments — spot any anomalies
Check D1 query analytics for slow queries (>50ms)
Review and update block lists in constants.js if new abuse patterns discovered
Check Resend dashboard for email delivery failures
📋 Common Operational Tasks — Step by Step
How to manually grant PRO to a user
📋 Two ways to grant PRO
- 1Via Admin Panel (easiest): Go to alshorty.com/admin → Users → search for user → click "Grant PRO" button.
- 2Via CLI:
wrangler d1 execute alshorty-db-prod --remote --command="UPDATE users SET plan='pro' WHERE email='user@email.com'" - 3Ask user to log out and log back in to refresh their session with the new plan.
How to delete a malicious/spam link
📋 Delete a link
- 1Via Admin Panel: alshorty.com/admin → Links → find the link code → click Delete.
- 2Via CLI:
wrangler kv key delete "link:CODE" --namespace-id <SHORTY_LINKS_ID> - 3Also disable the user if it's part of a larger abuse pattern: Admin → Users → Disable account.
How to enable maintenance mode (kill switch)
📋 Kill switch
- 1Via Admin Panel: alshorty.com/admin → System → "Enable Maintenance Mode" toggle.
- 2Via CLI:
wrangler kv key put "system:kill_switch" "true" --namespace-id <SHORTY_LIMITS_ID> - 3All public requests return 503. Admin endpoints (/__admin/*) still work.
- 4To disable: delete the key:
wrangler kv key delete "system:kill_switch" --namespace-id <SHORTY_LIMITS_ID>
How to temporarily reduce pricing for testing
📋 Change pricing
- 1Via Admin Panel: alshorty.com/admin → Config → Pricing section → change values → Save. Takes effect immediately, no redeploy.
- 2For Razorpay test mode: Change
RAZORPAY_KEY_IDsecret to arzp_test_xxxkey and redeploy worker. Test cards work without charging real money. - 3Minimum Razorpay order amount is ₹1 (100 paise). Set
pricing.monthly.inrto 1 in Admin Config for testing.
⚠️ Remember to restore real pricing and switch back to live Razorpay keys before going live!
How to reset a user's password / send new magic link manually
📋 Help a user log in
- 1Ask the user to visit alshorty.com/auth and request a new magic link. Resend sends it automatically.
- 2If email not arriving, check Resend dashboard → Logs → filter by their email address.
- 3If their account is disabled, re-enable via Admin Panel → Users → find user → Enable.
How to add a new blog post to the site
📋 Publishing a blog post
- 1Open
ui/src/lib/blog-data.ts. Add a new object to the array with:slug, title, excerpt, content, date, readTime, category, coverImage. - 2Add a cover image to
ui/public/blog-images/your-slug.webp(1200×630px, <80KB). - 3Regenerate sitemap:
node generate-sitemap.mjsfrom project root. - 4Commit and push:
git add -A && git commit -m "add blog: your title" && git push. Pages auto-deploys.
How to restore from a database backup
📋 Restore procedure
🚨 This overwrites the current database. Only do this in an emergency. Double-check the backup file is the right one.
- 1Get your latest backup .sql file from wherever you store backups.
- 2Enable maintenance mode first (kill switch) so no new writes happen during restore.
- 3
wrangler d1 execute alshorty-db-prod --remote --file=your-backup.sql - 4Verify row counts:
wrangler d1 execute alshorty-db-prod --remote --command="SELECT COUNT(*) FROM users; SELECT COUNT(*) FROM clicks" - 5Disable maintenance mode. Test login, link creation, redirects.
How to roll back the Worker to previous version
📋 Rollback procedure
- 1Fastest method:
cd worker && wrangler rollback— rolls back to the previous deployment instantly. - 2Specific version:
wrangler deployments list→ find the version ID →wrangler rollback <VERSION_ID> - 3Via Dashboard: Cloudflare → Workers → alshorty-v3 → Deployments → find a good deployment → "Rollback to this version".
- 4Test immediately after rollback:
curl https://api.alshorty.com/health
🩺 Health Check Commands — Run These After Every Deploy
# 1. Worker alive?
curl https://alshorty.com/health
# Expected: {"status":"ok","version":"3.0","ts":1234567890}
# 2. All KV namespaces + D1 green?
curl https://api.alshorty.com/api/status
# Expected: all bindings showing "ok"
# 3. Shortening works?
curl -X POST https://api.alshorty.com/api/shorten \
-H "Content-Type: application/json" \
-d '{"url":"https://google.com"}'
# Expected: {"ok":true,"code":"abc123","short_url":"https://alshorty.com/link/abc123"}
# 4. Redirect page showing?
curl -I https://alshorty.com/link/abc123
# Expected: HTTP/2 200 (the redirect page HTML, not a 302)
# 5. sitemap.xml accessible?
curl -I https://alshorty.com/sitemap.xml
# Expected: HTTP/2 200 with Content-Type: text/xml
# 6. Admin stats (replace YOUR_ADMIN_SECRET)
curl https://alshorty.com/__admin/stats \
-H "Authorization: Bearer YOUR_ADMIN_SECRET"
# Expected: JSON with user counts, link counts, etc.
# 7. Watch live logs
wrangler tail --format pretty