Tagstash is a tag-first bookmarking app built with React, Cloudflare Pages Functions, and D1. It includes user accounts, email verification, Stripe-powered Pro subscriptions, import tooling, and a companion browser extension.
The official hosted version is available at https://tagsta.sh/
Tagstash is active and in real-world use.
Current status at a glance:
- User registration, login, and JWT-backed sessions are working
- Email verification and resend flows are live
- Free and Pro tiers are implemented
- Stripe Checkout and Stripe Billing Portal are integrated
- Admin controls exist for managing user roles and membership tiers
- Bookmark CRUD, tag cloud filtering, import flow, and search are in place
- Opt-in public profiles let users share a read-only, tag-filterable view of their bookmarks
- Browser extension support exists in the companion TagstashExtension project
- Manual and background refresh keep the bookmark list in sync with saves made via the browser extension
- A dedicated Tag Management page supports merging tags together
- Tag-first bookmark organization
- Bookmark title, URL, description, and tag management
- Search, sorting, and tag query filtering
- Free tier with a 50-bookmark limit
- Pro tier with unlimited bookmarks
- Stripe Checkout for upgrades ($3/month or $36/year — same rate either way)
- Stripe Billing Portal for subscription management
- Email verification via Cloudflare Email Sending
- Opt-in public profiles (
/u/:username) for sharing your bookmarks via a link, with per-bookmark privacy control and tag filtering - Public JSON API (
/api/profiles/:username, optionally tag-filtered) for embedding your bookmarks on other sites, with ready-to-copy URLs in Settings - Super admin controls for managing users, roles, and tiers
- Three theme options (Slate, Midnight, Light), with your pick synced to your account when logged in
- Responsive UI for desktop and mobile
- Firefox/browser extension companion for saving the current tab quickly
- Refresh button plus a quiet 5-minute background poll so bookmarks saved elsewhere (e.g. the browser extension) show up without a page reload
- Random button for jumping straight to a random bookmark in edit mode
- Tag Management page (
/tags) for merging two tags into one, with a confirmation step since it can't be undone - Self-service username, email, and password changes, plus a forgot-password email flow
- Personal API keys (Settings) for programmatic access to your account
The commercial hosted version of Tagstash is available at https://tagsta.sh/
That hosted service is the official paid offering run by Stone Dragon Media LLC. This repository is source-available primarily so people can study the codebase; see LICENSE.md for what else is permitted.
- React 18
- React Router
- Vite
- Axios
- Context API
- lucide-react
- Cloudflare Pages Functions
- Cloudflare D1
- bcryptjs
- jose
- Cloudflare Email Sending
- Stripe REST API
- Node.js 18+
- npm
- Wrangler / Cloudflare account for deployment workflows
git clone https://github.com/jmusick/Tagstash.git
cd Tagstash
npm installCreate .dev.vars for local Cloudflare Functions development.
Required or commonly used values:
JWT_SECRETSUPER_ADMIN_EMAILCLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_IDfor email verification (Cloudflare Email Sending)API_KEY_ENCRYPTION_SECREToptional, defaults toJWT_SECRETbehavior in app usageSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_MONTHLY_PRICE_IDSTRIPE_ANNUAL_PRICE_IDAPP_URLfor hosted redirect URLs
For the frontend, optionally create .env and set:
VITE_API_URLif you do not want to use the default local proxy
npm run setup:dbRun frontend and API together:
npm run dev:allOr run them separately:
npm run dev
npm run dev:apiDefault local URLs:
- Frontend: http://localhost:3000
- API: http://localhost:5000/api
Tagstash is designed for Cloudflare Pages + D1.
Production setup includes:
- D1 migrations applied locally and remotely
- Cloudflare Pages secrets for Stripe and email
- Stripe webhook endpoint wired to
/api/billing/webhook
STRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETSTRIPE_MONTHLY_PRICE_IDSTRIPE_ANNUAL_PRICE_IDAPP_URL
POST /api/auth/registerPOST /api/auth/loginGET /api/auth/meGET /api/auth/verify-emailPOST /api/auth/resend-verificationPUT /api/auth/usernamePUT /api/auth/emailPUT /api/auth/passwordPUT /api/auth/profile-publicPUT /api/auth/themeGET /api/auth/api-keysPOST /api/auth/api-keysDELETE /api/auth/api-keys/:idDELETE /api/auth/api-keys/:id/permanentPOST /api/auth/forgot-passwordPOST /api/auth/reset-password
GET /api/auth/admin/usersPATCH /api/auth/admin/users/:idDELETE /api/auth/admin/users/:id
GET /api/billing/plansGET /api/billing/statusPOST /api/billing/checkout-sessionPOST /api/billing/portal-sessionPOST /api/billing/webhook
POST /api/support— contact form submission (email + message), gated by Cloudflare Turnstile
GET /api/bookmarksGET /api/bookmarks/:idPOST /api/bookmarksPUT /api/bookmarks/:idDELETE /api/bookmarks/:idGET /api/bookmarks/by-url— lookup by exact URL, used to detect duplicates (e.g. from the browser extension)POST /api/bookmarks/meta— fetch a page's title metadataPOST /api/bookmarks/meta-description— fetch a page's description metadataPOST /api/bookmarks/importGET /api/bookmarks/tags/allPOST /api/bookmarks/tags/mergePOST /api/bookmarks/tags/:id/favoritePOST /api/bookmarks/:id/favoritePOST /api/bookmarks/:id/private
GET /api/profiles/:username— unauthenticated; returns a user's public, non-private bookmarks and tags if they've opted in viaPUT /api/auth/profile-public
This is a stable, documented public API meant for third-party consumption (not just internal page-support data) — Settings surfaces ready-to-copy URLs for it. Responses are JSON with CORS enabled for all origins, so it can be fetched directly from another site's frontend.
Optional repeatable ?tag= query param filters to bookmarks that have all of the given tags (AND):
GET /api/profiles/alice
GET /api/profiles/alice?tag=poe2
GET /api/profiles/alice?tag=poe2&tag=guides
Example response:
{
"profile": { "username": "alice", "member_since": "2026-01-01T00:00:00.000Z" },
"bookmarks": [
{
"id": 1,
"title": "PoE2 Leveling Guide",
"url": "https://example.com/guide",
"description": "...",
"favicon_url": "https://...",
"created_at": "2026-01-01T00:00:00.000Z",
"updated_at": "2026-01-02T00:00:00.000Z",
"tags": [{ "id": 1, "name": "poe2" }, { "id": 2, "name": "guides" }]
}
],
"tags": [{ "name": "poe2", "count": 3 }, { "name": "guides", "count": 1 }],
"appliedTags": ["poe2", "guides"]
}tags always lists the full public tag set for that user, regardless of any ?tag= filter applied, so a consumer can discover what other tags are available.
tagstash/
├── d1/
│ └── migrations/
├── functions/
│ └── api/
├── public/
├── src/
│ ├── api/
│ ├── components/
│ ├── context/
│ ├── App.jsx
│ └── main.jsx
├── .dev.vars
├── package.json
├── vite.config.js
└── wrangler.toml
npm run dev- Run the frontend dev servernpm run dev:api- Run local Cloudflare Pages Functions and D1npm run dev:all- Run frontend and API togethernpm run setup:db- Apply local D1 migrationsnpm run build- Create a production buildnpm run preview- Preview the production build locallynpm run lint- Run ESLint
Tagstash is source-available under the custom Tagstash Non-Commercial License (TNCL) v1.0.
In plain English:
- You can run Tagstash yourself for free
- You can modify it for your own non-commercial use
- You cannot sell it
- You cannot charge for hosting it
- You cannot bundle it into a paid product or service
- You cannot make money from it in any way without explicit written permission
Read the full terms in LICENSE.md.
For commercial licensing inquiries, contact legal@tagsta.sh.