Slink is a self-hosted URL shortener built with Laravel 12, PostgreSQL, Redis, and Aurelia 2. It creates short links, groups them with tags, and records enough click data to show what is being used.
- Shorten URLs with auto-generated slugs
- Link titles and tagging
- Link notes, expiration, and click limits
- Optional password protection on links
- Bulk CSV import (via API + UI upload)
- Bulk actions (activate/deactivate/delete/tag) and CSV export
- Click tracking with basic analytics (device, browser, referrer, country)
- Analytics time ranges, trend chart, exports, and bot filtering
- Queue health indicator and link health checks
- API-first design with Bearer token authentication
- Token scopes (abilities) and audit logging
- Responsive workspace for desktop and mobile
Planned/extended features live in technical-spec.md.
- Backend: Laravel 12 (PHP 8.4+)
- Database: PostgreSQL 18
- Cache/Queue: Redis 8 + Horizon
- Frontend: Aurelia 2 + Tailwind CSS 4 + Vite
- Web server: Nginx (Docker)
cp .env.example .env
docker compose up --buildOpen the app at: http://localhost:8080
docker compose exec -T app php artisan db:seedIf SLINK_SEED_TOKEN is set in .env, that token will be used. Otherwise the seeder prints a generated token once. Paste it in the app via Settings → Bearer token to authorize the UI.
docker compose exec -T app php artisan migrateAll API routes are prefixed with /api/v1 and require a Bearer token unless noted. Link destinations must use HTTP or HTTPS.
GET /api/v1/links: List linksPOST /api/v1/links: Create link (slug is auto-generated)POST /api/v1/links/bulk: Bulk create linksPOST /api/v1/links/actions: Bulk actions (activate/deactivate/delete/tag/untag)GET /api/v1/analytics?query=&from=&to=&include_bots=1: Analytics with filters and time rangeGET /api/v1/links/{id}/stats: Link analyticsGET /api/v1/audit-logs: Recent audit activityGET /api/v1/user/stats: Account statsPOST /api/v1/user/tokens: Create API tokenDELETE /api/v1/user/tokens/{id}: Revoke API tokenGET /{slug}: Redirect (public)POST /{slug}/verify: Verify password-protected link (public)
CSV headers are optional. Supported columns:
original_url(orurl/link): requiredtitle: optionalexpires_at: optionalmax_clicks: optional
Example:
original_url,title
https://youtube.com,YouTube
https://news.ycombinator.com,Hacker NewsUse the analytics filter input with operators:
url:orlink:ororiginal_url:slug:title:contains:
Examples:
url:youtubetitle:"creator tools"contains:news slug:hn
Tokens can optionally declare abilities (scopes). If omitted, tokens default to full access (*).
Available abilities:
links:read,links:writetags:read,tags:writeanalytics:readuser:readtokens:writehealth:readaudit:read
Docker is the recommended setup. If you prefer local services, you will need:
- PHP 8.4+
- Node.js 20+
- PostgreSQL 18
- Redis 8
High-level steps:
cp .env.example .env
composer install
php artisan key:generate
php artisan migrate
npm install
npm run devServe the backend via your preferred web server or php artisan serve. Ensure APP_URL matches your local URL and that Postgres/Redis connection settings are correct.
- 401 Unauthorized in UI: Paste a valid token in Settings.
- Refreshing SPA routes yields 404: Routes are mapped in
routes/web.php. If you add new SPA paths, update the route list. - CSV import errors: Ensure each row has a valid URL in the first column or
original_urlheader.
app/: Laravel backendroutes/: API and web routingresources/js/src/: Aurelia 2 SPAresources/css/: Tailwind CSSdocker/: Nginx and container config
npm run dev
npm run typecheck
npm run build
composer test
vendor/bin/pint --test
npm audit
composer auditRun on-demand link health checks:
php artisan links:check-healthBy default it checks active links that have not been checked in the last 24 hours. Use --all to force a full sweep.
The UI shows queue status via:
GET /api/v1/health/queue
Please see CONTRIBUTING.md for guidelines on local setup, conventions, and PRs.
MIT