Cloudsteading
Cloudmark author-operated demo showing its bookmark list, categories and search; seeded demo collection, not a fresh Cloudsteading installation.
Cloudmark author-operated demo showing its bookmark list, categories and search; seeded demo collection, not a fresh Cloudsteading installation.

Cloudmark

Self-hosted bookmarks, categories and search on Cloudflare Workers and D1.

Cloudmark is a self-hosted Raindrop.io alternative built on Cloudflare (D1, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.

Source & license

Upstream license: AGPL-3.0

License TL;DR

You can use and change it, even commercially. If people use your modified version over a network, offer them its corresponding source under the AGPL. Sharing copies has source-sharing duties too. Sharing source code is different from sharing users’ content.

Explain AGPL v3 in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@wesleyel

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Free tier eligible within limits

A small personal bookmark collection can fit Workers Free and D1 Free allowances. API requests, CPU, database scans, writes and storage are bounded; this is source-based eligibility, not a measured zero-cost deployment.

Hosting requirements
  • Use your own Workers Free account and D1 database. Keep Worker execution within account-wide 100,000 requests/day and 10 ms CPU per invocation; bulk imports and token hashing require workload measurement.
  • D1 Free includes 5 million rows read/day, 100,000 rows written/day and 5 GB account-wide storage. Scans, rate-limit rows, indexes and migration writes count; exceeding free limits prevents further operations.
  • Keep a small collection within the reviewed 1,000-bookmark source limit. A collection read loads all its bookmarks, so browser filtering does not reduce the initial database scan.
  • Use the bundled static frontend. No R2, KV, external database, paid authentication or AI service is required by this target. Optional extension and favicon requests are separate from core hosting.
  • Replace author-owned identifiers and routes, apply D1 migrations and use workers.dev or your own domain. Back up tokens and data, review public/private settings and budget maintenance separately.
Check current pricing ↗
Review findings & limitations
  • The screenshot shows the author-operated seeded demo. Search was checked read-only; we did not create a collection, save bookmarks, install the extension or test a fresh deployment. The running demo version was not established against this source pin.
  • Collections default to public: anyone knowing the collection URL can read them. The reviewed source also supports private collections requiring the same token that permits writes. Private read-only invitations, user accounts and team permission roles are not established.
  • Write tokens live in browser localStorage and the bookmarklet. Bookmarklet saving sends the token in a query string; protect browser history, access logs and token backup files. Do not use private bookmarks without testing the configured access behavior.
  • Legacy migrated collections marked token_delivered=0 can issue a write token on the first collection read. Finish and review that migration path before exposing a legacy collection URL. Fresh collection creation defaults token_delivered to 1.
  • Replace the shipped author D1 ID, BASE_URL and custom-domain routes with your own resources. Apply all migrations before deployment; workers.dev can avoid a custom-domain purchase.
  • Source limits a collection to 1,000 bookmarks and an import request to 500 items. Collection reads load all bookmarks; import, rate limiting and index updates consume D1 operations. Limits and token delivery use multiple database steps; concurrent behavior was not tested.
  • The optional unpacked Chrome extension requests unrestricted host access to support self-hosted origins. It was not installed or audited. Automatic favicon URLs use Google; custom images and external favicon requests need a separate privacy assessment.
  • AGPL-3.0 source requires copyleft review, particularly when distributing changes or offering modified software over a network. Retain the original license and notices. Dependencies and asset rights need their own check.
Sources checked 01/10/2026

Repository snapshot: 7a3ee47. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • raindrop-io ↗

    Cloudmark is a universal cloud bookmark management tool that allows you to easily save and access your bookmarks from anywhere.

  • workers ↗

    "main": "./src/worker/index.ts"

  • d1 ↗

    "binding": "DB", "database_name": "cloudmark"

  • free-tier-eligible ↗

    | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation |

  • free-tier-eligible ↗

    | Rows read | 5 million / day |

  • free-tier-eligible ↗

    | Rows written | 100,000 / day |

  • free-tier-eligible ↗

    | Storage (per GB stored) | 5 GB (total) |

  • free-tier-eligible ↗

    Requests to static assets are free and unlimited.

  • free-tier-eligible ↗

    export const MAX_BOOKMARKS_PER_MARK = 1000;

  • AGPL-3.0 ↗

    13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is com

  • architecture ↗

    "run_worker_first": ["/api/*"]

  • architecture ↗

    app.route("/api", api); app.route("/api", bookmarklet);

  • architecture ↗

    return c.env.ASSETS.fetch(c.req.raw);

  • architecture ↗

    getCollectionPageData(c.env.DB, mark, viewToken)

  • architecture ↗

    await verifyWriteToken(viewToken!, existing.write_token_hash)

  • architecture ↗

    await db.batch(statements.slice(i, i + CHUNK));

Documented public demo screenshot · wesleyel/cloudmark repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

Raindrop.io logoRaindrop.io ↗

Saving URLs with titles and notes, organizing them by category, filtering and searching a personal bookmark collection. This is an editorial comparison; Raindrop tags, full-text page search, permanent copies, automated broken-link checks and team permissions were not established in Cloudmark.

See supporting source ↗
external SaaS target
varies
→ D1 + Workers

How it works

The shape of Cloudmark on Cloudflare, and how it stacks up against the rented tools it replaces.

Diagram target: wrangler.jsonc. Other repository deployments are listed in the source evidence below; they are not required by this target.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points1
cloudmark
wrangler.jsonc
↓
App
cloudmark
entry
Cloudflare Workers
Entrypoint: ./src/worker/index.ts
↓

Configuration and workflow sources

Reviewed commit 7a3ee4782ab7. This configuration evidence comes from reading source files. Execution checks, when available, appear in the project's runtime review.

Deployment configuration · 1 file
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2025-04-01

cloudmark · default

Entrypoint: ./src/worker/index.ts

Static assets: directory not declared · single-page-application · Worker first: ["/api/*"]

Configured route patterns: cloudmark.site · www.cloudmark.site

  • DB → D1
  • ASSETS → Static assets

Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

Runtime source · handlers, binding usage and workflow steps

Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

src/worker/index.ts ↗
  • L9 · app.use("/api/*")
  • L11 · app.get("/api/health")
  • L13 · app.route("/api")
  • L14 · app.route("/api")

Environment references: c.env.ASSETS

src/worker/routes/api.ts ↗
  • L30 · api.get("/collections/:mark")
  • L46 · api.post("/collections/claim")
  • L66 · api.put("/collections/settings")
  • L86 · api.post("/collections/regenerate-token")
  • L106 · api.post("/bookmarks")
  • L121 · api.put("/bookmarks/:uuid")
  • L137 · api.delete("/bookmarks/:uuid")
  • L153 · api.post("/bookmarks/import")
  • L25 · jsonError calls (conditional paths may differ): c.json

Environment references: c.env.DB

src/worker/routes/bookmarklet.ts ↗
  • L103 · bookmarklet.get("/add")
  • L15 · redirectWithStatus calls (conditional paths may differ): url.searchParams.set, c.redirect, url.toString
  • L27 · savedPage calls (conditional paths may differ): opts.message.slice, encodeURIComponent, detail.replace, c.html
  • L86 · isDuplicateError calls (conditional paths may differ): test
  • L90 · finish calls (conditional paths may differ): redirectWithStatus, savedPage

Environment references: c.env.DB

src/worker/services/bookmarks.ts ↗
  • L39 · getFavicon calls (conditional paths may differ): hostname.replace
  • L49 · isAcceptableMark calls (conditional paths may differ): Boolean, test, mark.includes
  • L58 · getCollectionPageData calls (conditional paths may differ): isDemoMark, isAcceptableMark, getCollection, rowToSettings, Boolean, verifyWriteToken, getBookmarksData, generateWriteToken, hashWriteToken, rotateCollectionToken, markTokenDelivered, isValidMarkFormat
  • L130 · requireWriteAccess calls (conditional paths may differ): isDemoMark, isAcceptableMark, checkRateLimit, getCollection, verifyWriteToken
  • L161 · claimCollection calls (conditional paths may differ): isDemoMark, checkRateLimit, getCollection, verifyWriteToken, rowToSettings, updateCollectionSettings, hashWriteToken, createCollection
  • L215 · saveCollectionSettings calls (conditional paths may differ): requireWriteAccess, getCollection, rowToSettings, trim, updateCollectionSettings
  • L237 · regenerateToken calls (conditional paths may differ): requireWriteAccess, hashWriteToken, updateCollectionToken
  • L249 · createBookmark calls (conditional paths may differ): isDemoMark, isAcceptableMark, checkRateLimit, getCollection, isValidMarkFormat, hashWriteToken, createCollection, verifyWriteToken, assertUnderBookmarkLimit, findBookmarkByUrl, rowToSettings, input.category.trim, crypto.randomUUID, getFavicon, toISOString, insertBookmark
  • L343 · updateBookmarkRecord calls (conditional paths may differ): requireWriteAccess, findBookmarkByUuid, findBookmarkByUrl, getFavicon, toISOString, updateBookmark
  • L404 · deleteBookmarkRecord calls (conditional paths may differ): requireWriteAccess, deleteBookmark
  • L429 · importBookmarks calls (conditional paths may differ): isDemoMark, isAcceptableMark, checkRateLimit, getCollection, isValidMarkFormat, hashWriteToken, createCollection, verifyWriteToken, countBookmarks, getBookmarksForMark, existing.map, item.url.trim, seen.has, seen.add, existingUrls.has, toISOString, Number.isNaN, d.getTime, d.toISOString, hostname.replace
src/shared/db.ts ↗
  • L55 · getCollection calls (conditional paths may differ): first, bind, db.prepare
  • L73 · getBookmarksForMark calls (conditional paths may differ): all, bind, db.prepare, map
  • L88 · getBookmarksData calls (conditional paths may differ): getCollection, getBookmarksForMark
  • L100 · countBookmarks calls (conditional paths may differ): first, bind, db.prepare
  • L111 · createCollection calls (conditional paths may differ): toISOString, run, bind, db.prepare
  • L156 · updateCollectionSettings calls (conditional paths may differ): toISOString, run, bind, db.prepare
  • L182 · rotateCollectionToken calls (conditional paths may differ): toISOString, run, bind, db.prepare
  • L211 · markTokenDelivered calls (conditional paths may differ): run, bind, db.prepare
  • L221 · updateCollectionToken calls (conditional paths may differ): toISOString, run, bind, db.prepare
  • L237 · touchCollection calls (conditional paths may differ): toISOString, run, bind, db.prepare
  • L248 · insertBookmark calls (conditional paths may differ): run, bind, db.prepare, touchCollection
  • L274 · updateBookmark calls (conditional paths may differ): run, bind, db.prepare, touchCollection
  • L299 · deleteBookmark calls (conditional paths may differ): run, bind, db.prepare, touchCollection
  • L315 · findBookmarkByUrl calls (conditional paths may differ): first, bind, db.prepare, rowToBookmark
  • L330 · findBookmarkByUuid calls (conditional paths may differ): first, bind, db.prepare, rowToBookmark
  • L345 · assertUnderBookmarkLimit calls (conditional paths may differ): countBookmarks
  • L360 · insertBookmarksBatch calls (conditional paths may differ): bookmarks.map, bind, db.prepare, db.batch, statements.slice, touchCollection
src/shared/security.ts ↗
  • L14 · generateWriteToken calls (conditional paths may differ): crypto.getRandomValues, bytesToBase64Url
  • L24 · generateSecureMark calls (conditional paths may differ): Math.floor, Math.random, toLowerCase, slice, replace, bytesToBase64Url, crypto.getRandomValues
  • L62 · isValidMarkFormat calls (conditional paths may differ): test, RESERVED_MARKS.has, mark.toLowerCase
  • L75 · isValidTokenFormat calls (conditional paths may differ): test
  • L87 · hashWriteToken calls (conditional paths may differ): encode, crypto.subtle.digest, join, map, Array.from, padStart, b.toString
  • L95 · verifyWriteToken calls (conditional paths may differ): isValidTokenFormat, hashWriteToken, timingSafeEqual
  • L106 · timingSafeEqual calls (conditional paths may differ): a.charCodeAt, b.charCodeAt
  • L117 · bytesToBase64Url calls (conditional paths may differ): String.fromCharCode, replace, btoa
  • L129 · checkRateLimit calls (conditional paths may differ): Date.now, first, bind, db.prepare, run
src/shared/favicon.ts ↗
  • L6 · makeMarkIcon calls (conditional paths may differ): toUpperCase, slice, escapeXml, encodeURIComponent
  • L36 · isEmojiIcon calls (conditional paths may differ): Boolean
  • L40 · emojiFromIcon calls (conditional paths may differ): value.slice
  • L45 · siteFaviconUrl calls (conditional paths may differ): hostname.replace, encodeURIComponent
  • L54 · isValidFaviconValue calls (conditional paths may differ): value.startsWith, value.slice, test
  • L76 · escapeXml calls (conditional paths may differ): replace, s.replace
  • L85 · sanitizeSvg calls (conditional paths may differ): replace, svgText.replace
src/client/lib/token-store.ts ↗
  • L17 · getStoredWriteToken calls (conditional paths may differ): localStorage.getItem, tokenKey
  • L26 · setStoredWriteToken calls (conditional paths may differ): localStorage.setItem, tokenKey
  • L39 · ensureLocalWriteToken calls (conditional paths may differ): getStoredWriteToken, generateWriteToken, setStoredWriteToken
  • L47 · clearStoredWriteToken calls (conditional paths may differ): localStorage.removeItem, tokenKey, backupAckKey
  • L57 · isBannerDismissed calls (conditional paths may differ): localStorage.getItem, bannerKey
  • L66 · dismissBanner calls (conditional paths may differ): localStorage.setItem, bannerKey
  • L75 · clearBannerDismiss calls (conditional paths may differ): localStorage.removeItem, bannerKey
  • L85 · isTokenBackupAcknowledged calls (conditional paths may differ): localStorage.getItem, backupAckKey
  • L94 · setTokenBackupAcknowledged calls (conditional paths may differ): localStorage.setItem, backupAckKey, localStorage.removeItem
  • L113 · buildTokenRecoveryPackage calls (conditional paths may differ): toISOString
  • L129 · downloadTokenBackup calls (conditional paths may differ): buildTokenRecoveryPackage, downloadJson, downloadTextFile, join, setTokenBackupAcknowledged
  • L149 · parseTokenRecoveryFile calls (conditional paths may differ): text.trim, trimmed.startsWith, JSON.parse, trimmed.match, test
  • L182 · listStoredTokenMarks calls (conditional paths may differ): localStorage.key, marks.push, key.slice, marks.sort
src/client/components/import-export.tsx ↗
  • L40 · ImportExportDialog calls (conditional paths may differ): useTranslations, useRef, useState, setPreview, setFileName, setProgress, resetImport, setTab, onOpenChange, exportNetscapeHtml, downloadTextFile, toast.success, t, exportCloudmarkJson, downloadJson, file.text, parseBookmarkFile, toast.error, toast.message, setBusy
src/shared/netscape-bookmarks.ts ↗
  • L23 · exportNetscapeHtml calls (conditional paths may differ): replace, s.replace, byCategory.get, list.push, byCategory.set, escape, sort, byCategory.entries, localeCompare, lines.push, Math.floor, getTime, lines.join
  • L82 · parseNetscapeHtml calls (conditional paths may differ): re.exec, test, folderStack.pop, trim, decodeHtml, stripTags, folderStack.push, name.slice, getAttr, Number, slice, folderStack.join, href.slice, title.slice, Number.isFinite, results.push, desc.slice
  • L161 · exportCloudmarkJson calls (conditional paths may differ): toISOString, bookmarks.map
  • L181 · parseCloudmarkJson calls (conditional paths may differ): JSON.parse, Array.isArray, String, test, out.push, url.slice, slice, b.description.slice, Math.floor, getTime
  • L215 · parseBookmarkFile calls (conditional paths may differ): text.trim, trimmed.startsWith, parseCloudmarkJson, test, parseNetscapeHtml
  • L235 · getAttr calls (conditional paths may differ): attrs.match
  • L244 · stripTags calls (conditional paths may differ): s.replace
  • L248 · decodeHtml calls (conditional paths may differ): replace, s.replace, String.fromCharCode, Number, parseInt
src/shared/bookmarklet.ts ↗
  • L11 · buildBookmarkletCode calls (conditional paths may differ): baseUrl.replace, join, JSON.stringify, encodeURIComponent
  • L36 · bookmarkletBodyForDisplay calls (conditional paths may differ): code.startsWith, decodeURIComponent, code.slice
Build and deployment pipeline · 1 GitHub Actions workflow

Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

Release Chrome Extension · .github/workflows/release-extension.yml ↗

Triggers: push, workflow_dispatch

release · no job dependencies declared

  1. Check out repositoryactions/checkout@v4
  2. Set up Node.jsactions/setup-node@v4
  3. Enable pnpmcorepack enable corepack prepare pnpm@9.14.4 --activate
  4. Install dependenciespnpm install --frozen-lockfile
  5. Typecheckpnpm typecheck
  6. Build extensionpnpm build:extension
  7. Resolve release tagif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "RELEASE_TAG=${DISPATCH_TAG}" >> "$GITHUB_ENV" else echo "RELEASE_TAG=${GITHUB_REF_NAME}" >> "$GITHUB_ENV" fi
  8. Validate versionMANIFEST_VERSION=$(node -p "require('./dist-extension/manifest.json').version") TAG_VERSION=${RELEASE_TAG#chrome-v} if [ "$MANIFEST_VERSION" != "$TAG_VERSION" ]; then echo "Manifest version $MANIFEST_VERSION does not match tag $RELEASE_TAG" exit 1 fi
  9. Package extensioncd dist-extension zip -qr ../cloudmark-chrome.zip . cd .. sha256sum cloudmark-chrome.zip > cloudmark-chrome.zip.sha256 test "$(unzip -Z1 cloudmark-chrome.zip | grep -c '^manifest.json$')" -eq 1 for path in popup.html background.js icons/icon-128.png _locales/en/messages.json _locales/zh_CN/messages.json; do unzip -Z1 cloudmark-chrome.zip | grep -Fxq "$path" done
  10. Write release notescat > release-notes.md <<'EOF' ## Install 1. Download `cloudmark-chrome.zip` below. 2. Unzip it to a permanent directory. 3. Open `chrome://extensions` in Chrome. 4. Enable **Developer mode**. 5. Click **Load unpacked** and select the unzipped directory. Keep the directory after installation. Chrome cannot install this store-external build with one click. EOF
  11. Create or update GitHub ReleaseTITLE="Cloudmark Chrome Extension ${RELEASE_TAG#chrome-v}" if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then gh release upload "$RELEASE_TAG" \ cloudmark-chrome.zip \ cloudmark-chrome.zip.sha256 \ --clobber else gh release create "$RELEASE_TAG" \ cloudmark-chrome.zip \ cloudmark-chrome.zip.sha256 \ --target "$GITHUB_SHA" \ --title "$TITLE" \ --notes-file release-notes.md fi
package.json ↗
  • build: tsc -b && vite build
  • build:extension: vite build --config vite.config.extension.ts
  • build:all: pnpm build && pnpm build:extension
  • deploy: pnpm build && wrangler deploy

Full upstream document by @wesleyel · README.md · snapshot 7a3ee47

Cloudmark

AGPL LICENSE Try It Online Deploy to Cloudflare

中文文档

Introduction

Cloudmark is a universal cloud bookmark management tool that allows you to easily save and access your bookmarks from anywhere. No login or registration required — create a collection ID (mark) plus a secret write token, install the bookmarklet, and start saving pages.

Try it online: https://cloudmark.site/

Key Features

  • 🔑 No Registration: Access your collection with a unique mark; writes require a write token
  • 🔖 One-Click Save: Bookmarklet embeds mark + write token
  • 🧩 Chrome Extension: Save the active tab with an existing or new category
  • 🏷️ Category Management: Custom categories for organization
  • ⌨️ Keyboard-first: / search, j/k navigate, n/e/d CRUD, ? help
  • 📋 High density: Compact list with instant filter / sort
  • 🎨 Custom icons: Emoji, letter marks, or upload SVG/ICO/PNG
  • 🌐 Cross-Device Access: Read anywhere; copy the write token to another device to write
  • 📝 Detailed Descriptions: Optional notes per bookmark
  • 🌍 Multi-Language Support: English and Chinese
  • 🗄️ Cloudflare D1: Relational storage on the edge

Security Model

Capability Requirement
View collection Know the mark (URL)
Add / edit / delete Valid write token for that mark
Bookmarklet save mark + token query params
  • Write tokens are hashed (SHA-256) in D1; plaintext is only stored in the browser (localStorage) and in the bookmarklet.
  • Rate limits and field length limits protect against abuse.

Quick Start

  1. Visit https://cloudmark.site/doc
  2. Generate a mark and write token (or customize the mark)
  3. Drag the bookmarklet to your browser bookmarks bar
  4. Click the bookmarklet while browsing to save pages
  5. Open https://cloudmark.site/your-mark to manage bookmarks

Chrome Extension

The Manifest V3 extension saves the active tab with an existing or new category and adds page/link actions to Chrome's context menu.

Install from GitHub Releases
  1. Download cloudmark-chrome.zip.
  2. Unzip it to a permanent directory.
  3. Open chrome://extensions and enable Developer mode.
  4. Click Load unpacked and select the unzipped directory.
  5. Configure the collection mark, write token, and Cloudmark server in the popup.

Chrome does not support one-click installation of store-external extensions for regular users. Keep the unzipped directory after loading the extension. New versions are published on the GitHub Releases page.

For local development, build and load dist-extension/ instead:

pnpm install
pnpm build:extension

See extension/README.md for permissions and development details.

Local Development

Prerequisites
  • Node.js 20+ and pnpm
  • Cloudflare account (for preview and deployment)
Install Dependencies
pnpm install
D1 setup
  1. Create a D1 database:
pnpm exec wrangler d1 create cloudmark
  1. Put the returned database_id into wrangler.jsonc → d1_databases[0].database_id.

  2. Apply migrations:

# local (Miniflare)
pnpm db:migrate:local

# production
pnpm db:migrate:remote
Development Mode
pnpm dev

Visit http://localhost:3000.

Local Preview with Cloudflare
pnpm db:migrate:local
pnpm preview
Build and Deploy

One-click deploy (creates a Cloudflare Worker from this repo):

Deploy to Cloudflare

Or from your machine:

pnpm db:migrate:remote
pnpm run deploy

After the first deploy, set d1_databases[0].database_id in wrangler.jsonc (or via the dashboard) and run migrations.

Cloudflare Configuration

D1 Database
"d1_databases": [
  {
    "binding": "DB",
    "database_name": "cloudmark",
    "database_id": "your-d1-database-id",
    "migrations_dir": "migrations"
  }
]
Environment Variables
  • BASE_URL — public site URL in Worker vars (optional; client uses current origin)

Technology Stack

License

This project is open-sourced under the AGPL-3.0 license.

Contributing

Issues and Pull Requests are welcome!

  1. Fork the project
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Contact

If you have any questions, please contact us through GitHub Issues.

Frequently asked about Cloudmark

What is Cloudmark?+

Cloudmark is a self-hosted Raindrop.io alternative built on the Cloudflare developer platform. Self-hosted bookmarks, categories and search on Cloudflare Workers and D1.

What does Cloudmark replace?+

Cloudmark is listed as an alternative to Raindrop.io. Compare the features and tradeoffs before migrating.

What Cloudflare primitives does Cloudmark use?+

Cloudmark is built on D1, Workers.

How much does Cloudmark cost to run?+

A small personal bookmark collection can fit Workers Free and D1 Free allowances. API requests, CPU, database scans, writes and storage are bounded; this is source-based eligibility, not a measured zero-cost deployment. Use your own Workers Free account and D1 database. Keep Worker execution within account-wide 100,000 requests/day and 10 ms CPU per invocation; bulk imports and token hashing require workload measurement. D1 Free includes 5 million rows read/day, 100,000 rows written/day and 5 GB account-wide storage. Scans, rate-limit rows, indexes and migration writes count; exceeding free limits prevents further operations. Keep a small collection within the reviewed 1,000-bookmark source limit. A collection read loads all its bookmarks, so browser filtering does not reduce the initial database scan. Use the bundled static frontend. No R2, KV, external database, paid authentication or AI service is required by this target. Optional extension and favicon requests are separate from core hosting. Replace author-owned identifiers and routes, apply D1 migrations and use workers.dev or your own domain. Back up tokens and data, review public/private settings and budget maintenance separately. Check current Cloudflare pricing before deploying.

Is Cloudmark open source?+

The upstream repository declares the AGPL-3.0 license. Read its terms at https://raw.githubusercontent.com/wesleyel/cloudmark/7a3ee4782ab701aeb8b4edfb8288f876cb8bcb66/LICENSE. Source code and contributor credit are available at https://github.com/wesleyel/cloudmark.

Community rating

No ratings yet. Tried this project? Share your experience.

One rating per verified account. You can change or remove yours. Accounts are email verified; use of the software is self-reported.

Discuss your experience ↓
Sign in to rate

Discussion · 0

sign in to comment →
No comments yet — be the first.