{"id":49200936,"url":"https://github.com/dend/slacktide","last_synced_at":"2026-04-23T14:02:50.320Z","repository":{"id":345438549,"uuid":"1185835946","full_name":"dend/slacktide","owner":"dend","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-19T06:53:13.000Z","size":326,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-19T18:54:40.325Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dend.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-19T01:45:39.000Z","updated_at":"2026-03-19T06:53:16.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dend/slacktide","commit_stats":null,"previous_names":["dend/slacktide"],"tags_count":null,"template":false,"template_full_name":"dend/template","purl":"pkg:github/dend/slacktide","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dend%2Fslacktide","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dend%2Fslacktide/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dend%2Fslacktide/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dend%2Fslacktide/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dend","download_url":"https://codeload.github.com/dend/slacktide/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dend%2Fslacktide/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32183351,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-23T11:42:27.955Z","status":"ssl_error","status_checked_at":"2026-04-23T11:42:18.877Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-23T14:02:49.039Z","updated_at":"2026-04-23T14:02:50.300Z","avatar_url":"https://github.com/dend.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"media/logo.svg\" alt=\"Slacktide logo\" width=\"128\" /\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eSlacktide\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/dend/slacktide/actions/workflows/ci.yml\"\u003e\u003cimg src=\"https://github.com/dend/slacktide/actions/workflows/ci.yml/badge.svg\" alt=\"CI\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/dend/slacktide/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/dend/slacktide\" alt=\"License\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://workers.cloudflare.com\"\u003e\u003cimg src=\"https://img.shields.io/badge/platform-Cloudflare%20Workers-F38020?logo=cloudflare\u0026logoColor=white\" alt=\"Cloudflare Workers\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/dend/slacktide/commits/main\"\u003e\u003cimg src=\"https://img.shields.io/github/last-commit/dend/slacktide\" alt=\"Last commit\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/dend/slacktide/stargazers\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/dend/slacktide\" alt=\"GitHub stars\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003e [!NOTE]\n\u003e Want to see it in action first? No backend required - the admin panel ships with a full mock mode:\n\u003e ```bash\n\u003e npm install\n\u003e npm run dev:mock -w packages/admin\n\u003e ```\n\u003e Then open [http://localhost:5173](http://localhost:5173) to explore the UI with dummy data.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"media/slacktide.gif\" alt=\"Slacktide admin dashboard\" width=\"720\" /\u003e\n  \u003cbr /\u003e\n  \u003cem\u003eThe Slacktide admin dashboard\u003c/em\u003e\n\u003c/p\u003e\n\nA self-hosted URL shortener on Cloudflare Workers with an admin dashboard, click analytics, and API key support.\n\n## Table of Contents\n\n- [Architecture](#architecture)\n- [Features](#features)\n- [Prerequisites](#prerequisites)\n- [Setup](#setup)\n  - [1. Clone and install](#1-clone-and-install)\n  - [2. Create a GitHub OAuth App](#2-create-a-github-oauth-app)\n  - [3. Provision infrastructure](#3-provision-infrastructure)\n  - [4. Run migrations](#4-run-migrations)\n  - [5. Set secrets](#5-set-secrets-manual-setup-only)\n  - [6. Seed the first owner](#6-seed-the-first-owner)\n  - [7. Deploy](#7-deploy)\n  - [8. Register the OAuth client](#8-register-the-oauth-client)\n- [Local Development](#local-development)\n  - [Worker](#worker)\n  - [Admin](#admin)\n  - [Mock mode](#mock-mode-no-backend-needed)\n  - [OAuth client registration](#oauth-client-registration)\n- [API Reference](#api-reference)\n  - [Authentication](#authentication)\n  - [Links](#links)\n  - [Analytics](#analytics)\n  - [Users](#users)\n  - [API Keys](#api-keys)\n  - [Auth](#auth)\n  - [Public](#public)\n- [Admin Dashboard](#admin-dashboard)\n- [Environment Variables](#environment-variables)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Architecture\n\n- **Worker** - Cloudflare Worker (Hono) serving as an OAuth 2.1 resource server and redirect engine\n- **D1** - SQLite database for links, clicks, and users\n- **KV** - Two namespaces: `LINKS_KV` for redirect caching, `OAUTH_KV` for token storage\n- **Admin** - SvelteKit static SPA (Cloudflare Pages) with Chart.js analytics\n\n```mermaid\nsequenceDiagram\n    box Admin SPA (OAuth 2.1 PKCE)\n        participant Admin\n    end\n    box Cloudflare Worker\n        participant Worker as OAuthProvider\n        participant API as apiHandler (/api/*)\n    end\n    participant GitHub\n\n    note over Admin,GitHub: Browser authentication flow\n    Admin-\u003e\u003eWorker: GET /authorize\n    Worker-\u003e\u003eGitHub: Redirect to GitHub OAuth\n    GitHub--\u003e\u003eWorker: GET /auth/github/callback?code=...\n    Worker--\u003e\u003eAdmin: Redirect with authorization code\n    Admin-\u003e\u003eWorker: POST /oauth/token (code + PKCE verifier)\n    Worker--\u003e\u003eAdmin: Access token + refresh token\n    Admin-\u003e\u003eAPI: GET /api/* (Bearer \u003coauth token\u003e)\n    API--\u003e\u003eAdmin: JSON response\n\n    note over API: API key authentication flow\n    participant Client as API Client\n    Client-\u003e\u003eWorker: GET /api/* (Bearer stk_...)\n    Worker-\u003e\u003eWorker: resolveExternalToken: D1 lookup + hash verify\n    Worker-\u003e\u003eAPI: Forwards with UserProps\n    API--\u003e\u003eClient: JSON response\n\n    note over Worker: Public routes (no auth)\n    participant Visitor\n    Visitor-\u003e\u003eWorker: GET /:slug\n    Worker--\u003e\u003eVisitor: 302 redirect\n```\n\n## Features\n\n- **Short links** - auto-generated base62 slugs or custom vanity slugs with optional expiration\n- **Tags** - organize links with tags, filter by multiple tags in the dashboard\n- **Analytics** - per-link click tracking with breakdowns for referrers, countries, devices, browsers, and operating systems\n- **OAuth 2.1 API** - Bearer token auth via `@cloudflare/workers-oauth-provider`\n- **API keys** - `stk_` prefixed keys for programmatic access (CI/CD, scripts, integrations), managed from the Settings page\n- **GitHub authentication** - login with GitHub, no separate account needed\n- **RBAC** - `owner` (manage users, links, and API keys) and `admin` (manage own links/analytics/keys) roles\n- **User management** - owners add/remove authorized users from the admin panel\n- **KV caching** - sub-millisecond redirects with 24h KV TTL\n\n## Prerequisites\n\n- Node.js 20+\n- Cloudflare account with Workers, D1, KV, and Pages access\n- GitHub OAuth App (one for production, one for local dev)\n\n## Setup\n\n### 1. Clone and install\n\n```bash\ngit clone \u003crepo-url\u003e\ncd slacktide\nnpm install\ncp wrangler.toml.example wrangler.toml  # then edit with your values\n```\n\n### 2. Create a GitHub OAuth App\n\nGo to **GitHub → Settings → Developer settings → OAuth Apps → New OAuth App**:\n\n| Field | Value |\n|---|---|\n| Application name | `Slacktide` |\n| Homepage URL | `https://your-domain.example.com` |\n| Authorization callback URL | `https://your-domain.example.com/auth/github/callback` |\n\nNote the **Client ID** and generate a **Client Secret**.\n\n### 3. Provision infrastructure\n\n#### Option A: Terraform (recommended)\n\n```bash\ncd terraform\ncp terraform.tfvars.example terraform.tfvars\n# Edit terraform.tfvars with your values\nterraform init\nterraform apply\n```\n\nTerraform provisions everything (D1, KV, Worker, Pages). After apply, grab the resource IDs from the `wrangler_toml_snippet` output and paste them into `wrangler.toml`.\n\n#### Option B: Manual (wrangler CLI)\n\n```bash\n# D1 database\nwrangler d1 create slacktide-links\n\n# KV namespaces\nwrangler kv namespace create LINKS_KV\nwrangler kv namespace create OAUTH_KV\n```\n\nThen copy the example config and fill in your resource IDs:\n\n```bash\ncp wrangler.toml.example wrangler.toml\n# Edit wrangler.toml with your D1 database ID, KV namespace IDs,\n# GITHUB_CLIENT_ID, and ADMIN_ORIGIN\n```\n\n### 4. Run migrations\n\n```bash\nwrangler d1 execute slacktide-links --remote --file=migrations/0001_create_links.sql\nwrangler d1 execute slacktide-links --remote --file=migrations/0002_create_clicks.sql\nwrangler d1 execute slacktide-links --remote --file=migrations/0003_create_users.sql\nwrangler d1 execute slacktide-links --remote --file=migrations/0004_create_api_keys.sql\nwrangler d1 execute slacktide-links --remote --file=migrations/0005_api_keys_restrict_delete.sql\n```\n\n### 5. Set secrets (manual setup only)\n\nSkip if you used Terraform - secrets are already configured.\n\n```bash\nwrangler secret put GITHUB_CLIENT_SECRET\n```\n\n### 6. Seed the first owner\n\n```bash\n# Automatically resolves your GitHub profile and inserts you as owner\nnpm run seed:owner YOUR_GITHUB_USERNAME -- --remote\n```\n\nPulls your GitHub profile and inserts you as the first owner in production D1. Omit `-- --remote` for local dev. Safe to re-run.\n\n### 7. Deploy\n\n```bash\nnpm run deploy:worker\nnpm run deploy:admin\n```\n\n### 8. Register the OAuth client\n\nAfter the worker is deployed, register the admin SPA as an OAuth client:\n\n```bash\ncurl -X POST https://your-domain.example.com/oauth/register \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"client_name\": \"Slacktide Admin\",\n    \"redirect_uris\": [\"https://admin.your-domain.example.com/callback\"],\n    \"token_endpoint_auth_method\": \"none\",\n    \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n    \"response_types\": [\"code\"]\n  }'\n```\n\nThis returns a `client_id`. Set it as `VITE_OAUTH_CLIENT_ID` in the admin SPA's environment and redeploy.\n\n## Local Development\n\n### Worker\n\nCreate `.dev.vars` in the project root:\n\n```\nGITHUB_CLIENT_SECRET=your_dev_github_client_secret\n```\n\nCreate a **separate** GitHub OAuth App for local dev with callback URL `http://localhost:8787/auth/github/callback` and set its client ID in `wrangler.toml` under `[env.dev.vars]`.\n\n```bash\nnpm run dev\n```\n\n### Admin\n\n```bash\nVITE_API_URL=http://localhost:8787 VITE_OAUTH_CLIENT_ID=your_dev_client_id npm run dev:admin\n```\n\n### Mock mode (no backend needed)\n\n```bash\nnpm run dev:mock -w packages/admin\n```\n\nRuns the admin SPA with `VITE_MOCK=true` - no worker or database needed. All API calls return fake data, and mutations reset on refresh.\n\n### OAuth client registration\n\nRegister a dev OAuth client:\n\n```bash\ncurl -X POST http://localhost:8787/oauth/register \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"client_name\": \"Slacktide Admin (dev)\",\n    \"redirect_uris\": [\"http://localhost:5173/callback\"],\n    \"token_endpoint_auth_method\": \"none\",\n    \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n    \"response_types\": [\"code\"]\n  }'\n```\n\n## API Reference\n\nFull spec in [`openapi.yaml`](openapi.yaml) (also served at `/api/openapi.json`). All `/api/*` routes require a Bearer token:\n\n### Authentication\n\n#### OAuth 2.1 (browser)\n\nThe admin SPA uses OAuth 2.1 with PKCE via GitHub. See setup steps above.\n\n#### API keys (programmatic)\n\nFor CI/CD, scripts, and integrations that can't go through the browser OAuth flow. Create keys from the admin Settings page.\n\nKeys use the format `stk_\u003ckeyId\u003e_\u003csecret\u003e` (**s**lack**t**ide **k**ey) and work with the same `Authorization: Bearer` header. The worker resolves `stk_` tokens against D1 before falling back to OAuth.\n\n```bash\n# Example: list links with an API key\ncurl -H \"Authorization: Bearer stk_aB3kZ9mX_...\" https://your-domain.example.com/api/links\n```\n\nSecrets are SHA-256 hashed at rest (shown once at creation). Keys inherit the creating user's role and are deleted when the user is removed. Rotate by creating a new key, updating consumers, then revoking the old one.\n\n### Links\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/api/links?page=\u0026per_page=\u0026search=` | Bearer | List links (paginated, search matches slug, URL, and tags) |\n| `POST` | `/api/links` | Bearer | Create link |\n| `GET` | `/api/links/:id` | Bearer | Get link by ID |\n| `PUT` | `/api/links/:id` | Bearer | Update link |\n| `DELETE` | `/api/links/:id` | Bearer | Deactivate link |\n\n### Analytics\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/api/analytics/overview` | Bearer | Dashboard stats |\n| `GET` | `/api/analytics/:slug` | Bearer | Per-link analytics |\n\n### Users\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/api/users` | Bearer (owner) | List all users |\n| `POST` | `/api/users` | Bearer (owner) | Add user by GitHub username |\n| `DELETE` | `/api/users/:id` | Bearer (owner) | Remove user |\n\n### API Keys\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `POST` | `/api/keys` | Bearer | Create API key (returns secret once) |\n| `GET` | `/api/keys` | Bearer | List current user's keys (owners: `?user_id=X`) |\n| `GET` | `/api/keys/:keyId` | Bearer | Get key metadata (own keys, or any as owner) |\n| `DELETE` | `/api/keys/:keyId` | Bearer | Revoke key (own keys, or any as owner) |\n\n### Auth\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/api/me` | Bearer | Current user profile |\n\n\u003e **Bearer** = OAuth access token or API key (`stk_*`). Both use `Authorization: Bearer \u003ctoken\u003e`.\n\u003e\n\u003e **Bearer (owner)** = same, but requires the `owner` role.\n\n### Public\n\n| Method | Path | Auth | Description |\n|---|---|---|---|\n| `GET` | `/:slug` | None | Redirect to destination URL |\n| `GET` | `/health` | None | Health check |\n| `GET` | `/.well-known/oauth-authorization-server` | None | OAuth server metadata |\n\n## Admin Dashboard\n\n- **Dashboard** - overview stats (total links, clicks today/this week), top links with click counts, clicks-over-time chart, top countries\n- **Links** - create/edit/deactivate/delete links, search by slug/URL/tag, filter by status and tags (multi-select), per-link analytics with charts and breakdown tables\n- **Users** (owner only) - add/remove users by GitHub username, assign roles\n- **Settings** - create and revoke API keys\n\n## Environment Variables\n\n| Variable | Where | Description |\n|---|---|---|\n| `ADMIN_ORIGIN` | `wrangler.toml` | Admin SPA origin for CORS |\n| `GITHUB_CLIENT_ID` | `wrangler.toml` | GitHub OAuth App client ID |\n| `GITHUB_CLIENT_SECRET` | `wrangler secret` | GitHub OAuth App client secret |\n| `VITE_API_URL` | Admin env | Worker URL (defaults to same-origin) |\n| `VITE_OAUTH_CLIENT_ID` | Admin env | Registered OAuth client ID |\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for details.\n\n## License\n\nMIT - see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdend%2Fslacktide","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdend%2Fslacktide","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdend%2Fslacktide/lists"}