Write a notebook of markdown pages. Share it with one link. No signup, no login.
Live at sharepad.in · Report a bug · Privacy policy
- What is SharePad?
- Features
- How it works
- Quick start
- Environment variables
- Database setup
- Image uploads (Cloudflare R2)
- Expiry and cleanup
- Security and privacy
- Analytics (optional)
- Project structure
- API routes
- GitHub Actions
- Deployment
- Tech stack
- License
SharePad is a zero-friction note-sharing app. You create a notebook — a collection of markdown pages behind a single URL — and hand out the link. Readers open it in their browser with no account. You keep a secret edit link to change content, adjust settings, or delete the notebook later.
It is built for situations where you need to share formatted text quickly: meeting notes, project docs, checklists, readme drafts, temporary handoffs, or anything that deserves more structure than a chat message but less ceremony than a wiki.
Free forever. No signup required.
| Feature | Description |
|---|---|
| Multi-page notebooks | One link, many pages — sidebar index, reorder, duplicate, delete |
| Markdown + GFM | Tables, task lists, strikethrough, autolinks, fenced code blocks |
| Syntax highlighting | Code blocks rendered with rehype-highlight |
| Split editor | Write / split / read modes with synced scroll |
| Auto-save | Debounced save while typing |
| Undoable toolbar | Bold, links, code blocks, etc. participate in Ctrl+Z |
| Tab indentation | Tab / Shift+Tab indent lists and blocks in the editor |
| Keyboard shortcuts | ⌘S save, ⌘B bold, ⌘I italic, ⌘/ cycle modes, ? help |
| Version history | Last 10 drafts per page, restorable |
| Page templates | Blank, meeting notes, project brief, journal, weekly plan |
Import .md |
Load a local markdown file into the editor |
| Copy markdown | One-click copy of the current page |
| Formatting help | In-editor popup explaining markdown syntax |
| Feature | Description |
|---|---|
| Quick paste-and-share | Drop text at /quick — title and slug generated automatically |
| Custom slugs | Pick your URL: /n/kitchen-reno, checked live for availability |
| Two links | View link (/n/slug) to share; edit link (/e/token) to keep private |
| Open editing | Optionally let anyone with the view link write content |
| Password lock | Optional bcrypt-hashed password before viewing |
| Read-only mode | Freeze content for everyone (edit token still works for the owner) |
| Burn after read | View link works exactly once |
| Visibility | Public (indexable), unlisted (link-only), or private (view link blocked) |
| QR code | Generate a QR for the view link in the share panel |
| Recover edit link | Paste a lost edit URL at /recover |
| Feature | Description |
|---|---|
| Paper textures | Ruled, grid, dotted, or plain |
| Typefaces | Handwritten, serif, sans, or monospace |
| Red margin rule | Notebook-style layout in view and edit modes |
| Print / PDF | /n/slug/print — clean serif document, table of contents, page breaks |
| Markdown export | Download the whole notebook as one .md file |
| Feature | Description |
|---|---|
| Paste, drop, or pick | Images go straight into markdown as  |
| Client-side compression | Resized to 1600px max edge, re-encoded as WebP before upload |
| GIF support | Animated GIFs pass through untouched |
| Cloudflare R2 storage | Zero egress cost at any volume |
| Feature | Description |
|---|---|
| Anonymous comments | Per-page comments with a display name, no account |
| View counter | Anonymous view count on each notebook |
| Saved notebooks | Edit tokens stored in browser localStorage, listed on the home page |
| Custom error pages | Themed 404 and error screens |
| SEO landing pages | Use-case pages targeting common search phrases |
| Accessibility | Skip links, ARIA labels, keyboard navigation, tooltips |
There are no user accounts. Ownership is token-based.
Create notebook
→ server generates a 32-byte edit token (shown once)
→ SHA-256 hash stored in Postgres
→ browser saves token in localStorage
→ user copies the edit URL
| URL | Purpose |
|---|---|
/ |
Home page |
/quick |
Paste text, get a link instantly |
/new |
Full notebook creation form |
/recover |
Recover access via edit link |
/n/{slug} |
View a notebook (read mode) |
/n/{slug}/print |
Print-friendly / PDF view |
/e/{token} |
Edit a notebook (owner mode) |
/privacy |
Privacy policy |
- View link — read pages, comment (if enabled), export
- Edit link — change content, pages, settings, delete notebook
- Open editing — visitors with the view link can edit content only; settings, expiry, and deletion always require the edit token
This means a visitor to an open-edit notebook cannot lock the owner out or change the password.
- Node.js 20+
- A Supabase project (free tier works)
- Optional: Cloudflare R2 bucket for image uploads
git clone https://github.com/Varshithvhegde/sharepad.git
cd sharepad
npm install
cp .env.example .env.local
# Fill in your Supabase keys (see below)
npm run devOpen http://localhost:3000.
| Command | Description |
|---|---|
npm run dev |
Start development server (Turbopack) |
npm run build |
Production build |
npm run start |
Serve production build |
npm run lint |
Run ESLint |
Copy .env.example to .env.local and fill in the values.
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-publishable-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
NEXT_PUBLIC_SITE_URL=http://localhost:3000NEXT_PUBLIC_SITE_URL is used for canonical URLs, the sitemap, llms.txt, Open Graph tags, and links in the share panel. Set it to your production domain (e.g. https://sharepad.in) when deployed.
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN=phc_...
NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.comWithout a PostHog token, analytics is completely skipped.
All five must be set or image upload stays disabled:
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=sharepad
R2_PUBLIC_BASE_URL=https://pub-xxxxx.r2.devR2_PUBLIC_BASE_URL is the public URL from your bucket's Settings tab (enable the r2.dev subdomain or attach a custom domain like https://img.sharepad.in).
IMAGE_SWEEP_SECRET=any-long-random-stringUsed by the weekly GitHub Action to authenticate /api/images/sweep. Add the same value as a repository secret in GitHub.
SQL migrations live in supabase/migrations/. Apply them in order:
| Migration | What it adds |
|---|---|
20240101000000_initial_schema.sql |
Notebooks, pages, versions, comments, RLS, RPC functions |
20240102000000_paper_textures.sql |
Paper texture themes (ruled, grid, dot, plain) |
20240103000000_open_editing_and_fonts.sql |
Open editing toggle, font selection |
20240104000000_purge_expired_notebooks.sql |
Nightly expiry purge via pg_cron |
20240105000000_restrict_rpc_execution.sql |
Lock down RPC to service role |
20240106000000_images_and_rate_limits.sql |
Images table, rate limiting |
Option A — Supabase CLI (recommended):
supabase link --project-ref your-project-ref
supabase db pushOption B — SQL editor:
Open the Supabase dashboard → SQL editor → paste and run each migration file in order.
notebooks slug, title, edit_token_hash, password_hash, settings, expires_at
pages notebook_id, title, content, sort_order, icon, pinned
page_versions page_id, content (last 10 kept per page)
comments page_id, author_name, content
images notebook_id, object_key, byte_size, content_type
rate_limits bucket, identity, window_start, hits
Row-level security is enabled on all tables. The app reaches the database through the service role key on the server — there are no public write policies.
Images are stored in Cloudflare R2, not Supabase Storage. R2 charges nothing for egress, which matters when a widely shared notebook is opened repeatedly.
- User pastes, drops, or picks an image in the editor
- Browser compresses it (max 1600px edge, WebP at 82% quality; GIFs pass through)
- Server sniffs the file's magic bytes (not the declared Content-Type)
- Object stored in R2 under
{notebook_id}/{random}.{ext} - Row inserted in the
imagestable - Markdown link inserted:

| Limit | Value |
|---|---|
| Max file size (after compression) | 5 MB |
| Max images per notebook | 50 |
| Upload rate limit | 60 per IP per 10 minutes |
Rate limiting uses an atomic Postgres function so two simultaneous requests cannot both slip through.
| Trigger | What happens |
|---|---|
| Notebook deleted | All R2 objects for that notebook removed immediately |
| Notebook expired | Nightly DB purge removes the row; weekly sweep deletes orphaned R2 objects |
| Image removed from markdown | File stays in R2 until notebook deletion (not content-aware yet) |
Notebooks expire based on an absolute timestamp, not a rolling countdown.
| Setting | Default |
|---|---|
| Default lifetime | 10 days |
| Options | 1 day, 7 days, 10 days, 30 days, 90 days, 1 year, never |
- Expiry moment — view and print links return 404 immediately
- 3 days later —
pg_cronjob deletes the notebook row (pages, versions, comments cascade) - Burn-after-read — removed 1 day after the single view
The owner can still open the edit link during the 3-day grace window and extend the expiry.
-- Runs nightly at 03:15 UTC
select cron.schedule(
'purge-expired-notebooks', '15 3 * * *',
$$select public.purge_expired_notebooks()$$
);- Edit tokens are 32 random bytes, shown once at creation
- Only the SHA-256 hash is stored in the database
- Tokens are sent via the
X-Edit-Tokenheader on API requests - Tokens saved in browser
localStoragefor the "Saved notebooks" list
There is no end-to-end encryption. Notebook content is stored in plaintext in Postgres. Do not put secrets in a notebook unless you also password-lock it and accept the trust model.
Raw HTML is parsed and then sanitized against GitHub's schema before rendering. This is not optional — notebooks with open editing can be written to by anyone with the link, and an injected script would run on the owner's page (where the edit token lives in the URL).
Blocked: <script>, <style>, <iframe>, <object>, <embed>, <form>, event-handler attributes, javascript: URLs.
Allowed extras: <mark>, <abbr>, <figure>, <figcaption>, table alignment attributes.
Schema: lib/markdown-schema.ts
View passwords are hashed with bcrypt. A successful unlock sets an httpOnly cookie (sp_unlock_{slug}) for 24 hours.
Full details at /privacy. Summary:
- No accounts, no email collection
- Content stored in Supabase Postgres
- Edit tokens in browser localStorage only
- Optional PostHog analytics (no content, no tokens, no session recording on notebook pages)
- Third parties: Supabase (database), Cloudflare R2 (images, if enabled), PostHog (analytics, if enabled)
Analytics is off by default. Set NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN to enable it.
Privacy constraints baked in:
- Autocapture off — would record text people click on
- Session recording off on notebook routes — would record private notes being typed
- No content in events — only counts and setting names, never titles, slugs, or edit tokens
- Memory-only persistence — no cookies, no consent banner needed
Events are defined in lib/analytics.ts as a typed union.
sharepad/
├── app/ # Next.js App Router
│ ├── page.tsx # Home page
│ ├── quick/page.tsx # Paste-and-share
│ ├── new/page.tsx # Create notebook form
│ ├── recover/page.tsx # Recover edit link
│ ├── privacy/page.tsx # Privacy policy
│ ├── n/[slug]/ # View + print routes
│ ├── e/[token]/ # Edit route
│ ├── api/ # REST API routes
│ ├── llms.txt/ # AI/LLM discovery file
│ └── sitemap.ts, robots.ts # SEO
├── components/
│ ├── NotebookEditor.tsx # Main editor shell
│ ├── SettingsPanel.tsx # Notebook settings + delete
│ ├── SharePanel.tsx # Share links + QR
│ ├── MarkdownPreview.tsx # Rendered markdown
│ ├── editor/ # Toolbar, comments, history
│ └── marketing/ # Header, footer, FAQ, landing pages
├── lib/
│ ├── supabase/ # Client, server, admin clients
│ ├── api-auth.ts # Token validation
│ ├── images.ts # Upload limits + magic-byte sniffing
│ ├── r2.ts # Cloudflare R2 client
│ ├── rate-limit.ts # IP-based rate limiting
│ ├── notebooks.ts # TOC, expiry checks, sanitization
│ ├── local-storage.ts # Saved edit tokens
│ └── ...
├── supabase/migrations/ # Database schema (apply in order)
├── public/ # Static assets (icon, screenshot)
└── .github/workflows/ # Keep-alive + image sweep cron jobs
All write operations require the X-Edit-Token header (or open editing for content changes).
| Method | Route | Description |
|---|---|---|
POST |
/api/notebooks |
Create a notebook |
GET/PATCH/DELETE |
/api/notebooks/[id] |
Read, update, or delete a notebook |
POST |
/api/notebooks/unlock/[slug] |
Unlock password-protected notebook |
POST |
/api/pages |
Add a page |
PATCH/DELETE |
/api/pages/[id] |
Update or delete a page |
GET/POST |
/api/pages/[id]/comments |
List or add comments |
GET |
/api/pages/[id]/versions |
Page version history |
GET |
/api/export/[slug] |
Download notebook as markdown |
GET |
/api/slug-check?slug= |
Check slug availability |
POST |
/api/images |
Upload an image |
POST |
/api/images/sweep |
Remove orphaned R2 objects (authenticated) |
GET |
/api/ping |
Health check (used by keep-alive workflow) |
Two scheduled workflows keep production healthy:
.github/workflows/keep-alive.yml — pings /api/ping every 3 days so the free-tier Supabase project does not pause after 7 days of inactivity.
.github/workflows/sweep-images.yml — runs weekly, calls /api/images/sweep to delete R2 objects whose notebook no longer exists in Postgres.
Both workflows accept an optional SITE_URL repository variable (defaults to https://sharepad.in). The sweep workflow requires the IMAGE_SWEEP_SECRET repository secret.
SharePad is a standard Next.js app. Deploy to Vercel, Railway, or any Node.js host.
- Set all required environment variables in your hosting dashboard
- Apply Supabase migrations
- Set
NEXT_PUBLIC_SITE_URLto your production domain - (Optional) Configure R2 for image uploads
- (Optional) Add
IMAGE_SWEEP_SECRETto GitHub repository secrets - (Optional) Add PostHog token for analytics
The included keep-alive workflow prevents the project from pausing. If it does pause, wake it from the Supabase dashboard and the next scheduled ping will keep it alive.
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router, Turbopack) |
| Database | Supabase (Postgres + pg_cron) |
| Styling | Tailwind CSS 4 |
| Markdown | react-markdown + remark-gfm + rehype-highlight |
| Images | Cloudflare R2 via aws4fetch |
| Analytics | PostHog (optional) |
| Icons | Lucide React |
| Fonts | Kalam, Architects Daughter, Source Serif 4, Inter, JetBrains Mono |
MIT — see LICENSE if present, otherwise MIT applies to this repository.
Support the project: Ko-fi · GitHub Issues
