Skip to content

arhen/satset.io

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

8 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

satset.io

A fast, no-nonsense URL shortener. No login, no tracking, just get the job done.

Live: satset.io

Features

  • Instant - Short links generated locally (zero network wait)
  • Offline-First - Works without internet, syncs when back online
  • Anonymous - No login, no tracking, no cookies
  • Custom Aliases - Make your links readable
  • QR Codes - One-click download
  • Auto-Expiry - URLs auto-delete after 14 days

Tech Stack

Layer Technology
Runtime Bun (dev) + Cloudflare Workers (prod)
Backend Elysia
Frontend React 19
Routing TanStack Router (file-based)
Data Fetching TanStack Query
Forms React Hook Form + Zod
Styling Tailwind CSS v4
Animations Framer Motion
Database Cloudflare D1 (SQLite)
Cache Cloudflare KV

Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              satset.io                                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚                         Cloudflare Edge                                β”‚ β”‚
β”‚  β”‚                                                                        β”‚ β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                β”‚ β”‚
β”‚  β”‚   β”‚   Workers   │◄──►│     D1      β”‚    β”‚     KV      β”‚                β”‚ β”‚
β”‚  β”‚   β”‚  (Elysia)   β”‚    β”‚  (SQLite)   β”‚    β”‚   (Cache)   β”‚                β”‚ β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                β”‚ β”‚
β”‚  β”‚          β”‚                                     β”‚                       β”‚ β”‚
β”‚  β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                β”‚                                            β”‚
β”‚                                β–Ό                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚                           API Layer                                     β”‚β”‚
β”‚  β”‚                                                                         β”‚β”‚
β”‚  β”‚    POST /api/urls           Create short URL                            β”‚β”‚
β”‚  β”‚    GET  /api/urls/check     Check alias availability                    β”‚β”‚
β”‚  β”‚    GET  /api/redirect       Get redirect data                           β”‚β”‚
β”‚  β”‚                                                                         β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                β”‚                                            β”‚
β”‚                                β–Ό                                            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚                        React Frontend                                   β”‚β”‚
β”‚  β”‚                                                                         β”‚β”‚
β”‚  β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚β”‚
β”‚  β”‚   β”‚    Router    β”‚  β”‚    Query     β”‚  β”‚  Hook Form   β”‚                  β”‚β”‚
β”‚  β”‚   β”‚  (TanStack)  β”‚  β”‚  (TanStack)  β”‚  β”‚    + Zod     β”‚                  β”‚β”‚
β”‚  β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚β”‚
β”‚  β”‚                                                                         β”‚β”‚
β”‚  β”‚   routes/                                                               β”‚β”‚
β”‚  β”‚   β”œβ”€β”€ __root.tsx      Root layout                                       β”‚β”‚
β”‚  β”‚   β”œβ”€β”€ index.tsx       Home page (/)                                     β”‚β”‚
β”‚  β”‚   β”œβ”€β”€ privacy.tsx     Privacy page (/privacy)                           β”‚β”‚
β”‚  β”‚   └── $alias.tsx      Redirect page (/:alias)                           β”‚β”‚
β”‚  β”‚                                                                         β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Offline-First Flow

The app generates short URLs locally for instant UX, then syncs to the backend when the user commits (copy/QR download).

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           Offline-First Architecture                         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                              β”‚
β”‚   1. USER PASTES URL                                                         β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                                         β”‚
β”‚      β”‚  Paste URL  β”‚                                                         β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜                                                         β”‚
β”‚             β”‚                                                                β”‚
β”‚             β–Ό                                                                β”‚
β”‚   2. LOCAL GENERATION (instant, no network)                                  β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚      β”‚  β€’ Generate random alias locally                β”‚                     β”‚
β”‚      β”‚  β€’ Generate QR code (canvas-based)              β”‚                     β”‚
β”‚      β”‚  β€’ Display short URL immediately                β”‚                     β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚                             β”‚                                                β”‚
β”‚                             β–Ό                                                β”‚
β”‚   3. USER COMMITS (copy link or download QR)                                 β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚      β”‚  β€’ Add to localStorage sync queue               β”‚                     β”‚
β”‚      β”‚  β€’ Trigger background sync                      β”‚                     β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚                             β”‚                                                β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                 β”‚
β”‚              β”‚                             β”‚                                 β”‚
β”‚              β–Ό                             β–Ό                                 β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                          β”‚
β”‚   β”‚     ONLINE      β”‚           β”‚    OFFLINE      β”‚                          β”‚
β”‚   β”‚                 β”‚           β”‚                 β”‚                          β”‚
β”‚   β”‚  POST /api/urls β”‚           β”‚  Queue in       β”‚                          β”‚
β”‚   β”‚  Sync to D1     β”‚           β”‚  localStorage   β”‚                          β”‚
β”‚   β”‚  Mark as synced β”‚           β”‚  Show warning   β”‚                          β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                          β”‚
β”‚            β”‚                             β”‚                                   β”‚
β”‚            β”‚                             β–Ό                                   β”‚
β”‚            β”‚                  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                            β”‚
β”‚            β”‚                  β”‚  BACK ONLINE    β”‚                            β”‚
β”‚            β”‚                  β”‚                 β”‚                            β”‚
β”‚            β”‚                  β”‚  Process queue  β”‚                            β”‚
β”‚            β”‚                  β”‚  Retry w/ exp.  β”‚                            β”‚
β”‚            β”‚                  β”‚  backoff        β”‚                            β”‚
β”‚            β”‚                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β”‚
β”‚            β”‚                           β”‚                                     β”‚
β”‚            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                     β”‚
β”‚                        β–Ό                                                     β”‚
β”‚   4. SYNCED                                                                  β”‚
β”‚      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚
β”‚      β”‚  β€’ URL stored in Cloudflare D1                  β”‚                     β”‚
β”‚      β”‚  β€’ Cached in Cloudflare KV                      β”‚                     β”‚
β”‚      β”‚  β€’ Redirect ready at satset.io/:alias           β”‚                     β”‚
β”‚      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚
β”‚                                                                              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key points:

  • Alias + QR generated client-side (zero latency)
  • Sync only happens when user commits (copies link or downloads QR)
  • Failed syncs retry with exponential backoff (up to 5 attempts)
  • localStorage persists queue across page reloads
  • Online/offline listeners auto-trigger sync when connection restored

Project Structure

satset.io/
β”œβ”€β”€ public/                     # Static assets
β”‚   β”œβ”€β”€ favicon.svg
β”‚   β”œβ”€β”€ index.html
β”‚   └── og-image.png
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ api/                    # Backend (Cloudflare Worker)
β”‚   β”‚   β”œβ”€β”€ worker.ts           # Elysia routes
β”‚   β”‚   β”œβ”€β”€ schemas.ts          # TypeBox validation schemas
β”‚   β”‚   β”œβ”€β”€ lib/                # Utilities (rate limit, security, etc.)
β”‚   β”‚   └── db/
β”‚   β”‚       └── schema.sql
β”‚   β”œβ”€β”€ client/                 # Frontend (React)
β”‚   β”‚   β”œβ”€β”€ main.tsx            # Entry point
β”‚   β”‚   β”œβ”€β”€ routes/             # TanStack Router (file-based)
β”‚   β”‚   β”œβ”€β”€ components/         # Shared components
β”‚   β”‚   β”œβ”€β”€ hooks/              # Custom hooks
β”‚   β”‚   β”œβ”€β”€ lib/
β”‚   β”‚   β”‚   β”œβ”€β”€ sync.ts         # Offline sync logic
β”‚   β”‚   β”‚   β”œβ”€β”€ api.ts          # API client
β”‚   β”‚   β”‚   └── ...
β”‚   β”‚   └── global.css          # Tailwind
β”‚   └── index.ts                # Dev server (Bun)
β”œβ”€β”€ scripts/
β”‚   └── build.ts                # Production build
β”œβ”€β”€ tsr.config.json             # TanStack Router config
β”œβ”€β”€ wrangler.toml.example       # Cloudflare config template
└── package.json

Setup

bun install
cp wrangler.toml.example wrangler.toml
# Edit wrangler.toml with your Cloudflare IDs
bun run db:init
bun dev

Deploy

# Create Cloudflare resources
wrangler d1 create url-shortener-db
wrangler kv namespace create CACHE

# Update wrangler.toml with the IDs from above

# Initialize database
wrangler d1 execute url-shortener-db --remote --file=src/api/db/schema.sql

# Deploy
bun run deploy

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors