Securely shorten URLs with opt-in password protection, one-time use, expiration and more.
This shortener was realised with NextJS 15, Drizzle (SQLite/Turso) & Tailwind CSS. The actual backend is split up in service interfaces to provide maximum modularity and isolate responsibilities. You can find most services and backend under features/urls/server/services
.
The URL encryption uses AES-256-CBC, where each shortened URL is encrypted with a unique random 16-byte initialization vector (IV). Passwords are hashed using PBKDF2 with SHA-512, and the same seed is used as the salt for password hashing. The seed - and thus the IV and salt - are unique, or at least random, to each shortened URL and stored alongside them in the table for shortened URLs.
By integrating with Google's Safe Browsing API, this service checks the safety of URLs during shortening. If no Google API key is available, the URL is marked as insecure by default. However, this service does not require Google's API, since it is used as an interface to evaluate whether it is worth to show a redirection warning to a user (thus interrupting the flow), it is just a default implementation using Google's API.
This service tracks visits and URL decryption events. Each decryption counts as a "visit" even if the user doesn't click the shortened link directly, potentially offering more granular analytics in the future. The reasoning behind this is, that a user - as soon as they receive the endpoint as plaintext - can visit it themselves without using the shortener as redirection tool.
The shortener uses the score based "hidden" Google Recaptcha to verify incoming shortening requests. The library used for this is react-google-recaptcha-v3
. As of the current implementation, it is missing proper error handling for when the recaptcha fails.
Following dependencies also have had an impact:
react-google-recaptcha-v3
,framer-motion
,react-qr-code
,react-icons
,@radix-ui/*
,@paralleldrive/cuid2
, ...
The entire application is built with accessibility and responsiveness in mind. Tho, in the current implementation this mindset is not entirely matured nor tested yet. Expect future updates to correctly validate that behaviour.
![](https://private-user-images.githubusercontent.com/47287352/382980471-9a9274bd-8538-43b3-9d60-ccabd6aa0d47.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkxNzcxMzcsIm5iZiI6MTczOTE3NjgzNywicGF0aCI6Ii80NzI4NzM1Mi8zODI5ODA0NzEtOWE5Mjc0YmQtODUzOC00M2IzLTlkNjAtY2NhYmQ2YWEwZDQ3LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTAyMTAlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwMjEwVDA4NDAzN1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTk2ODA2OWY0YWFhNmEyMDAzMTU1YTU2NjE1NjgxZjU3MGM0OTVhMzE0OWRjZWNlYTk0MDc5YWE3NDQwZTczNjEmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.r5HrQWmSyTjCTdHdMgVGJ0D4tH3j--AvPJBRor6Odfg)
The default implementation uses Googles Safe Browsing API.
Following environment variables are required in order for this project to work in its entirety:
Variable Name | Description |
---|---|
NEXT_PUBLIC_URL * |
URL hostname (including protocol) of your website. For example: http://localhost:3000 , where the dynamic routing is appended. Or for example https://aparx.dev , where a link whose path is abc resolves to https://aparx.dev/abc . |
URL_ENCRYPTION_KEY * |
The default implementation expects this key, used to encrypt and decrypt endpoint URLs using AES-256 in CBC mode. The default encoding for this string is base64, thus this string must be a 32 byte base64 encoded cryptographic key. There is no key rotation by default. |
TURSO_DATABASE_URL * |
Used as the target database URL that Drizzle can connect to |
TURSO_AUTH_TOKEN * |
The auth token authenticating the Drizzle client to Turso |
NEXT_PUBLIC_RECAPTCHA_SITE_KEY * |
Google Recaptcha v3 Site Key (get them here) |
RECAPTCHA_SECRET_KEY * |
Google Recaptcha v3 Secret (get them here) |
GOOGLE_SAFETY_API_KEY |
Google API key used for Google's Safe Browsing API (v4) |
GOOGLE_SAFETY_CLIENT_ID |
Client - not user - used for analytical purposes by Google |
GOOGLE_SAFETY_CLIENT_VERSION |
Client ID - not user - used for analytical purposes by Google |
* = Required