Skip to content

Latest commit

ย 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ”— Shrinko

A URL shortening with click analytics, QR codes, password protection, and link-limiting built on Hono and Cloudflare Workers.

TypeScript Hono Cloudflare Supabase Zod

Shrinko transforms long, clunky URLs into clean, short, brandable links while keeping visitor privacy at the core. It is a full web platform: anonymous visitors can shorten links instantly, generate QR codes, apply password protection, set expiration dates or click limits, and track every click through a per-link analytics dashboard all without creating an account.

Registered users get more: a personal dashboard with aggregate statistics, an activity chart, per-link analytics pages, profile management, and a doubled rate-limit quota. On the backend, the entire service runs as a single Cloudflare Worker written in TypeScript with Hono, backed by Supabase, hardened with input validation, salted-hash cryptography, per-IP rate limiting, and strict security headers. Raw IP addresses are never stored only salted SHA-256 hashes so click analytics remain useful while visitors stay anonymous.

๐Ÿ“ธ Screenshot

Shrinko landing page with the URL shortener form and advanced options

The screenshot shows the Shrinko landing page: the hero shortener with the "Advanced Options" panel (custom alias, password protection, click limit, expiration, and QR generation toggles), the animated link-to-short-link illustration, the feature grid, the analytics showcase, and the footer.

๐Ÿ“‘ Table of Contents

๐ŸŒŸ Features

โšก Instant Shortening

  • Shortens any http/https URL with a single request no account required.
  • Generates 5-character random codes (a-zA-Z0-9) with a collision-retry loop (up to 1000 attempts) to guarantee uniqueness, or accepts custom aliases (5โ€“15 alphanumeric characters).
  • Custom aliases are validated server-side with Zod against pattern rules, a reserved-words list (hundreds of reserved terms, including routes, platform names, and brands), and a profanity filter.
  • Every short link returns a one-time monitor key that grants anonymous access to that link's analytics and deletion, without any login.

๐Ÿ” Password-Protected Links

  • Link owners can set a password when shortening; the password is stored only as a salted SHA-256 hash never in plaintext.
  • Visiting a protected link serves a dedicated unlock page instead of redirecting.
  • Wrong passwords show a clear error on the unlock form; the same form also reports rate-limit warnings (HTTP 429) when the visitor is throttled.
  • Correctly unlocked links log the click and redirect (303) exactly like public links.

โณ Time & Click Limits

  • Links can be limited by expiration date (days from creation), maximum click count, or both.
  • On every visit, the worker checks expires_at and the accumulated click counter; exhausted links return a 410 "Link Not Available" page instead of redirecting.
  • The monitor page displays remaining clicks / days left and marks links as Limited or Expired, so owners always know the current state.

๐Ÿ“Š Click Analytics

  • Every successful redirect is logged asynchronously with hashed IP, country (via Cloudflare's cf-ipcountry header), OS (parsed from the User-Agent), and referrer source (mapped to a known-sources list or extracted domain).
  • The per-link monitor page shows total clicks, unique clicks (computed from unique IP hashes), day-over-day change, a clicks-over-time Chart.js chart, and a full click history table.
  • The same analytics are available as JSON (when the request's Accept header is not text/html), and authenticated users get a per-link details endpoint with aggregated referrers, countries, browsers, and OS breakdowns.

๐Ÿ” Anonymous Monitoring & Self-Service Deletion

  • Each link receives a random 64-character monitor key; only its salted SHA-256 hash is stored in the database.
  • GET /:short_url/:monitor_key opens the analytics page (or JSON) no account needed.
  • DELETE /api/v1/:short_url/:monitor_key permanently deletes the link again, identity-free by design.
  • Authenticated users can also view analytics (/dashboard/analytics/:short_url) and delete links (DELETE /api/v1/my-links/:short_url) through their own account.

๐Ÿ“ฑ QR Code Generation

  • GET /api/v1/qr?url={url} renders a PNG QR code server-side (rate-limited to 10 requests/minute/IP).
  • The landing page can show and download the QR code immediately after shortening; the dashboard offers per-link QR codes through a modal.
  • QR responses are cached with Cache-Control: public, max-age=31536000, immutable.

๐Ÿ‘ค Accounts, Google OAuth & Dashboard

  • Custom registration/login with strict validation (name = 2+ letter words, username = 5โ€“15 lowercase alphanumeric, password = 8โ€“20 chars with upper/lower/digit/special), a salted SHA-256 credential hash, and session rotation on every login (a fresh 128-char session token is issued and stored hashed).
  • Google sign-in via Supabase's PKCE OAuth flow (the Supabase publishable key is exposed to the browser; the service-role key stays server-side only).
  • The dashboard fetches the user's links with per-link unique-click counts, renders aggregate stats (total/unique clicks, links created, protected/limited/expired counts) with today-vs-yesterday deltas, an activity chart, sorting (newest/oldest/most/least clicked/expired), pagination, QR modal, and a delete confirmation dialog.
  • Profile page supports inline editing of full name, username, and password all re-validated server-side.

๐Ÿ›ก Security Headers, Rate Limiting & Hardened Pages

  • Every response carries strict security headers: a locked-down Content-Security-Policy, HSTS (preload), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy, restrictive Permissions-Policy, Cross-Origin-Opener-Policy, and Cross-Origin-Resource-Policy.
  • A per-IP sliding-window rate limiter (60-second window, in-memory) protects every route: 10 req/min default, 20 req/min authenticated shortening, 10 req/min anonymous shortening, 120 req/min profile reads, 10 req/min profile writes and QR generation. HTML requests get styled 429 pages; API requests get JSON errors.
  • User-controlled strings rendered into HTML pass through an HTML-escape helper, and the redirect flow distinguishes not-found (404) from expired (410) links.
  • The environment loader validates required secrets at startup and fails fast with explicit messages if anything is missing.

๐Ÿ— Architecture

Shrinko runs as a single Cloudflare Worker. Static assets are served through the ASSETS binding, all routes are defined on one Hono app, and every dynamic path flows through shared middleware before reaching route-specific logic.

Browser / API Client
       โ”‚
       โ–ผ
Cloudflare Worker (Hono app src/index.ts)
       โ”‚
       โ”œโ”€โ”€ Security Headers Middleware (CSP, HSTS, X-Frame-Options, โ€ฆ)
       โ”œโ”€โ”€ Rate-Limit Middleware (per IP ร— per route, 60s window)
       โ”‚
       โ”œโ”€โ”€ Static Assets       (ASSETS binding: /public/*, robots.txt,
       โ”‚                         sitemap.xml, favicons)
       โ”œโ”€โ”€ Page Routes         (/, /login, /register, /dashboard, /profile,
       โ”‚                         /api-docs, /about, /contact, /support,
       โ”‚                         /privacy, /terms, /dashboard/analytics/:code)
       โ”œโ”€โ”€ Shorten & Link API  (POST /api/v1/shorten,
       โ”‚                         GET+DELETE /api/v1/my-links[/:code][/details])
       โ”œโ”€โ”€ Redirect & Monitor  (GET /:code, POST /api/v1/:code,
       โ”‚                         GET+DELETE /:code/:monitor_key)
       โ”œโ”€โ”€ Auth API            (POST /api/v1/register, /api/v1/login,
       โ”‚                         GET+POST /api/v1/profile)
       โ””โ”€โ”€ System API          (GET /api(/)v1(/)/health, GET /api/v1/qr)
              โ”‚
              โ–ผ
       Services & Libs (validation, salted-hash crypto, analytics parsing,
                        link creation, monitor payload assembly)
              โ”‚
              โ–ผ
       Supabase PostgreSQL (users, links, clicks)
              โ”‚
              โ–ผ
       HTTP Response (HTML page / JSON / 302-303 redirect / PNG)

Key design decisions visible in the code:

  • Redirect speed over bookkeeping click logging is fired via executionCtx.waitUntil so the 302 response is never delayed by the insert.
  • No raw identifiers in storage IPs, session IDs, monitor keys, and passwords are all stored as salted SHA-256 hashes.
  • Public pages are server-rendered the UI is embedded in TypeScript page modules with targeted inline JavaScript, keeping the worker self-contained.
  • Environment fail-fast config/env.ts throws at boot when SUPABASE_URL, SUPABASE_KEY, SUPABASE_PUBLISHABLE_KEY, or SALT are missing.

๐ŸŽฅ Demo

A live, deployed instance runs at https://s.usfahmed.dev shorten a link immediately from the landing page, open the returned monitor link for analytics, and explore the public pages (/api-docs, /about, /support, โ€ฆ).

Try the anonymous workflow in seconds:

# 1. Shorten a URL
curl -s -X POST https://s.usfahmed.dev/api/v1/shorten \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/very/long/path?utm_source=readme","type":"random","isLimited":false,"isProtected":false}'
# โ†’ {"status":"success","redirect_url":"/ab123/<64-char-monitor-key>"}

# 2. Visit your new short link (redirects with 302)
curl -sI https://s.usfahmed.dev/ab123 | findstr /i "location"

# 3. Open the analytics page returned above every subsequent click appears there.

๐Ÿ“– Case Study

Case Study

A comprehensive technical article explaining the architecture, data flow, cryptographic mechanisms, security decisions, implementation details, trade-offs, and future improvements behind Shrinko.

โš™๏ธ Installation

Prerequisites

  • Node.js (โ‰ฅ 18) and npm
  • A Cloudflare account with the Wrangler CLI available (npx wrangler --version)
  • A Supabase project (PostgreSQL) used for users, links, and clicks

Clone Repository

git clone <your-repository-url> Shrinko
cd Shrinko

Install Dependencies

npm install

Additional Setup

  1. Create the environment file from the template and fill in real values:

    cp .env.example .env
    Variable Purpose
    SUPABASE_URL Your Supabase project URL
    SUPABASE_KEY Service-role (secret) key server-side only, never used in page HTML
    SUPABASE_PUBLISHABLE_KEY Publishable key (sb_publishable_...) used by the browser Supabase client for Google OAuth
    SALT A random string of at least 20 characters used to salt all hashes (passwords, sessions, monitor keys, IPs)

    The worker refuses to start if any of these are missing (src/config/env.ts).

  2. Create the database tables in Supabase (the schema is not bundled; the app expects three tables):

    • users id, username (unique), auth (salted credential hash), full_name, sid (hashed session token)
    • links id, short_url (unique constraint the collision-retry logic relies on it), long_url, clicks, password (hash, nullable), monitor_key (hash), max_clicks (nullable), expires_at (nullable), user_id (nullable), created_at
    • clicks id, link_id, ip_hash, user_agent, referer, country, time_of_click
  3. Enable Google OAuth in Supabase Auth (Providers โ†’ Google) if you want the "Sign in with Google" flow to work.

๐Ÿš€ Usage

Development

Serve the worker locally with Wrangler (this also activates the ASSETS binding and nodejs_compat):

npx wrangler dev

Deployment

npx wrangler deploy

Using the Service

  1. Shorten a link paste a URL into the landing-page form. Toggle Advanced Options to add a custom alias, a password, a click limit, an expiration (days), or a generated QR code.
  2. Share copy the returned short URL (e.g. https://s.usfahmed.dev/ab123); authenticated users see it also on their dashboard.
  3. Protect & limit password-gated links show an unlock page; limited links stop redirecting (410) once their time or click budget is spent.
  4. Analyze follow the returned /<code>/<monitor_key> link to see the analytics page, or use the dashboard's Analytics button. Everything is also exposed as JSON for API clients:
    • GET /:code/:monitor_key โ†’ analytics HTML or JSON (based on Accept)
    • GET /api/v1/my-links/:code/details โ†’ aggregated stats (auth required)
  5. Manage delete a link anonymously via DELETE /:code/:monitor_key, or from the dashboard as a registered user.
  6. Generate QR codes anytime via GET /api/v1/qr?url={url}.

๐Ÿ“‚ Project Structure

Shrinko/
โ”‚
โ”œโ”€โ”€ public/                      # Static assets served via the ASSETS binding
โ”‚   โ”œโ”€โ”€ favicon.* / apple-touch-icon.png   # Favicon set (svg/ico/png)
โ”‚   โ”œโ”€โ”€ logo.png / logo.svg / og-image.png
โ”‚   โ”œโ”€โ”€ robots.txt / sitemap.xml
โ”‚   โ””โ”€โ”€ (feature & UI icons: link.png, qr.png, protected.png, clicks.png, โ€ฆ)
โ”‚
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ index.ts                 # Entry point loads env, registers routes
โ”‚   โ”œโ”€โ”€ app.ts                   # Hono app + security headers + asset serving
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ config/
โ”‚   โ”‚   โ””โ”€โ”€ env.ts               # Env validation (fail-fast) + Supabase client
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ routes/                  # HTTP layer
โ”‚   โ”‚   โ”œโ”€โ”€ index.ts             # Route registration + 404 page
โ”‚   โ”‚   โ”œโ”€โ”€ links.ts             # Shorten + my-links list/details/delete APIs
โ”‚   โ”‚   โ”œโ”€โ”€ redirect.ts          # Redirect, password unlock, monitor, delete-by-key
โ”‚   โ”‚   โ”œโ”€โ”€ auth.ts              # Register / login / profile APIs
โ”‚   โ”‚   โ”œโ”€โ”€ system.ts            # Health checks + QR generation
โ”‚   โ”‚   โ””โ”€โ”€ pages.ts             # HTML page routes (+ dashboard auth guard)
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ pages/                   # Server-rendered UI (TS-embedded HTML/JS)
โ”‚   โ”‚   โ”œโ”€โ”€ landing.ts           # Landing page: shorten form + advanced options
โ”‚   โ”‚   โ”œโ”€โ”€ dashboard.ts         # User dashboard: stats, chart, link management
โ”‚   โ”‚   โ”œโ”€โ”€ monitor.ts           # Per-link analytics page
โ”‚   โ”‚   โ”œโ”€โ”€ auth.ts              # Login / register pages (custom + Google OAuth)
โ”‚   โ”‚   โ”œโ”€โ”€ profile.ts           # Profile editing page
โ”‚   โ”‚   โ”œโ”€โ”€ password.ts          # Protected-link unlock form
โ”‚   โ”‚   โ”œโ”€โ”€ generic.ts           # Shared page shell (SEO meta, nav, footer)
โ”‚   โ”‚   โ”œโ”€โ”€ health.ts            # System status page
โ”‚   โ”‚   โ””โ”€โ”€ content.ts           # API docs / about / contact / support / privacy / terms
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ services/                # Business logic
โ”‚   โ”‚   โ”œโ”€โ”€ links.ts             # Link creation, click logging, monitor payloads
โ”‚   โ”‚   โ”œโ”€โ”€ analytics.ts         # Referrer mapping + user-agent parsing
โ”‚   โ”‚   โ””โ”€โ”€ auth.ts              # Session resolution (JWT or sid cookie)
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ middleware/
โ”‚   โ”‚   โ””โ”€โ”€ rate-limit.ts        # In-memory per-IP sliding-window limiter
โ”‚   โ”‚
โ”‚   โ”œโ”€โ”€ lib/
โ”‚   โ”‚   โ”œโ”€โ”€ crypto.ts            # Salted SHA-256 helpers + secure random strings
โ”‚   โ”‚   โ”œโ”€โ”€ validation.ts        # Zod schema, reserved-alias list, regexes
โ”‚   โ”‚   โ””โ”€โ”€ html.ts              # HTML escaping (XSS mitigation)
โ”‚   โ”‚
โ”‚   โ””โ”€โ”€ types/
โ”‚       โ””โ”€โ”€ index.ts             # Worker bindings type (ASSETS)
โ”‚
โ”œโ”€โ”€ .env.example                 # Environment template
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ tsconfig.json
โ””โ”€โ”€ wrangler.json                # Cloudflare Workers configuration

โš  Challenges & Solutions

Problem: Random-code collisions

Random 5-character codes drawn from a 62-character alphabet collide once the link count grows, and a naive insert would fail on the unique index.

Solution

The shorten route retries insertion in a loop detecting unique-violation errors from Supabase and regenerating the code, up to 1000 attempts (src/routes/links.ts). In practice this resolves collisions invisibly, and the loop exits with a friendly error if the namespace is ever exhausted.

Problem: Click logging must never slow down the redirect

The redirect itself has to be a fast 302, but logging a click involves a read-modify-write counter update plus an insert.

Solution

Click logging runs asynchronously after the response is prepared through executionCtx.waitUntil (with a fire-and-forget fallback when no execution context exists), and the whole routine is wrapped in try/catch a failed log entry is printed to console but never breaks the user's redirect.

Problem: Meaningful analytics without storing raw IP addresses

Tracking visitors by raw IP would be a privacy liability and contradicts the project's privacy-first positioning.

Solution

IPs are hashed with SHA-256(SALT + ip + SALT) at the edge and only the hash is stored. "Unique clicks" are computed by counting distinct hashes so the analytics remain accurate while the raw address is never persisted or exposed.

Problem: Anonymous link ownership no account, no way to manage the link

Links created without an account still need a management mechanism, but giving them a password or login would defeat the purpose.

Solution

Each link gets a random 64-character monitor key, returned exactly once in the shorten response. Only its salted hash is stored, and it grants scoped access to that single link's analytics page and a delete endpoint. Because the stored form is a hash, a database leak does not reveal working keys.

Problem: Alias squatting and abuse

Custom aliases short enough to serve as routes or brand names would let users squat platform names, routes, or worse.

Solution

Aliases are validated server-side with Zod against a large reserved-words list (routes like login, analytics, about, platform/brand names, and more), a profanity filter (leo-profanity), and a strict [a-zA-Z0-9]{5,15} pattern enforced on both the API and the live form on the landing page.

๐Ÿ”ฎ Future Improvements

  • Persistent, distributed rate limiting the current limiter is in-memory per worker instance; moving it to Cloudflare KV / Durable Objects (or Cloudflare's own rate-limit rules) would make quotas global.
  • Stronger password hashing migrate link and account password hashes from salted SHA-256 to a dedicated KDF such as bcrypt or Argon2id.
  • Link editing today links can only be deleted; editing the destination URL or limits without changing the short code is a natural next step.
  • Bulk operations & exports CSV export of links and analytics, multi-select delete.
  • Webhooks / developer API keys programmatic creation and monitoring without login sessions.
  • Custom domains & branded short domains per user.
  • Live analytics updates push click events to open monitor pages instead of requiring a reload.
  • Unit & integration tests the project currently has no test suite; adding one would lock in the security and validation behavior.

๐Ÿ›  Technologies Used

Programming Language

  • TypeScript

Web Framework

  • Hono

Runtime & Deployment

  • Cloudflare Workers (Wrangler, nodejs_compat, Workers Assets)

Database

  • Supabase (PostgreSQL) users, links, clicks

Validation

  • Zod (payload schemas, alias and password rules)
  • leo-profanity (alias profanity filter)

Analytics & Utilities

  • ua-parser-js (user-agent / OS parsing)
  • qr-image (server-side QR generation)
  • Chart.js (client-side analytics charts)
  • Supabase JS SDK (client-side Google OAuth)

๐Ÿ“„ License

Copyright ยฉ 2026 Youssef Ahmed Abdelfatah. All Rights Reserved.

This source code is provided for educational and learning purposes only.

You may view and study the code for personal educational purposes. You may not copy, reproduce, redistribute, publish, sell, use commercially, or incorporate any part of this project into another project without prior written permission from the author.

Unauthorized copying or commercial use is prohibited.

โš  Disclaimer

Shrinko is a personal, experimental web service. Short links are created and managed by its users, and the platform's analytics record only anonymized visit data (hashed IP addresses, country, OS, and referrer never raw IPs). Password protection, expiration, and click limits are convenience features and are not a guarantee of security or availability. Use the service responsibly and only for lawful purposes; the author assumes no liability for content shared through links created on the platform.

๐Ÿ“ซ Contact

Youssef Ahmed Abdelfatah

๐ŸŒ Portfolio
https://usfahmed.dev

๐Ÿ’ป GitHub
https://github.com/usfa7med

๐Ÿ’ผ LinkedIn
https://linkedin.com/in/usfahmed

โœ‰๏ธ Email
hello@usfahmed.dev

About

A URL shortener with click analytics, QR code generation, password protection, expiration & click limits, and anonymous monitoring keys. Built with Hono + TypeScript, deployed on Cloudflare, backed by Supabase.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages