diff --git a/README.md b/README.md
index e359c57..8439f2a 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,19 @@ Available on iOS and Android with the same powerful features and encryption.
- 🌐 **Cross-platform** - Responsive web app that works seamlessly on desktop, tablet, and mobile
- 🖨️ **Print support** - Clean printing with proper formatting
+### 🌐 Public Notes (Sharing)
+- 🔗 **Shareable links** - Publish any note with a unique, unguessable URL
+- 👤 **Optional author attribution** - Add your name or publish anonymously
+- 🎨 **Full formatting preserved** - Rich text, code blocks, images, and diagrams render beautifully
+- 📑 **Table of contents** - Collapsible TOC for easy navigation
+- 🌙 **Theme toggle** - Readers can switch between light and dark mode
+- 📱 **Mobile-friendly** - Responsive design for all screen sizes
+- 🔄 **Auto-sync** - Changes to your note automatically update the public version
+- ❌ **Instant unpublish** - Remove public access at any time (hard delete)
+- 🛡️ **Security hardened** - DOMPurify sanitization, rate limiting, no internal IDs exposed
+
+> ⚠️ **Important:** Publishing a note bypasses end-to-end encryption. An unencrypted copy is stored on our servers and anyone with the link can view it. Use this feature only for content you intend to share publicly.
+
### ⚡ Executable Code Blocks

diff --git a/package.json b/package.json
index 61b29fa..e8a1e70 100644
--- a/package.json
+++ b/package.json
@@ -88,9 +88,11 @@
"@tiptap/react": "^3.4.4",
"@tiptap/starter-kit": "^3.4.4",
"@tiptap/suggestion": "^3.4.4",
+ "@types/dompurify": "^3.2.0",
"@unhead/react": "^2.0.17",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "dompurify": "^3.3.0",
"highlight.js": "^11.11.1",
"lowlight": "^3.3.0",
"lucide-react": "^0.544.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 08a4b6c..e29bbba 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -95,6 +95,9 @@ importers:
'@tiptap/suggestion':
specifier: ^3.4.4
version: 3.4.4(@tiptap/core@3.4.4(@tiptap/pm@3.4.4))(@tiptap/pm@3.4.4)
+ '@types/dompurify':
+ specifier: ^3.2.0
+ version: 3.2.0
'@unhead/react':
specifier: ^2.0.17
version: 2.0.17(react@19.1.1)
@@ -104,6 +107,9 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ dompurify:
+ specifier: ^3.3.0
+ version: 3.3.0
highlight.js:
specifier: ^11.11.1
version: 11.11.1
@@ -3075,6 +3081,10 @@ packages:
'@types/d3@7.4.3':
resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
+ '@types/dompurify@3.2.0':
+ resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
+ deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
+
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -11966,6 +11976,10 @@ snapshots:
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
+ '@types/dompurify@3.2.0':
+ dependencies:
+ dompurify: 3.3.0
+
'@types/estree@1.0.8': {}
'@types/geojson@7946.0.16': {}
@@ -13604,7 +13618,7 @@ snapshots:
eslint: 9.36.0(jiti@2.6.1)
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1))
eslint-plugin-expo: 1.0.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2)
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1))
eslint-plugin-react: 7.37.5(eslint@9.36.0(jiti@2.6.1))
eslint-plugin-react-hooks: 5.2.0(eslint@9.36.0(jiti@2.6.1))
globals: 16.4.0
@@ -13644,7 +13658,7 @@ snapshots:
tinyglobby: 0.2.15
unrs-resolver: 1.11.1
optionalDependencies:
- eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1))
+ eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1))
transitivePeerDependencies:
- supports-color
@@ -13663,6 +13677,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ eslint-module-utils@2.12.1(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1)):
+ dependencies:
+ debug: 3.2.7
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2)
+ eslint: 9.36.0(jiti@2.6.1)
+ eslint-import-resolver-node: 0.3.9
+ eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1))
+ transitivePeerDependencies:
+ - supports-color
+
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)):
dependencies:
debug: 3.2.7
@@ -13683,6 +13708,35 @@ snapshots:
- supports-color
- typescript
+ eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@9.36.0(jiti@2.6.1)))(eslint@9.36.0(jiti@2.6.1)):
+ dependencies:
+ '@rtsao/scc': 1.1.0
+ array-includes: 3.1.9
+ array.prototype.findlastindex: 1.2.6
+ array.prototype.flat: 1.3.3
+ array.prototype.flatmap: 1.3.3
+ debug: 3.2.7
+ doctrine: 2.1.0
+ eslint: 9.36.0(jiti@2.6.1)
+ eslint-import-resolver-node: 0.3.9
+ eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.36.0(jiti@2.6.1))
+ hasown: 2.0.2
+ is-core-module: 2.16.1
+ is-glob: 4.0.3
+ minimatch: 3.1.2
+ object.fromentries: 2.0.8
+ object.groupby: 1.0.3
+ object.values: 1.2.1
+ semver: 6.3.1
+ string.prototype.trimend: 1.0.9
+ tsconfig-paths: 3.15.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2)
+ transitivePeerDependencies:
+ - eslint-import-resolver-typescript
+ - eslint-import-resolver-webpack
+ - supports-color
+
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@4.4.4)(eslint@9.36.0(jiti@2.6.1)):
dependencies:
'@rtsao/scc': 1.1.0
diff --git a/src/App.tsx b/src/App.tsx
index d1d3d7c..fef0c0e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -19,6 +19,7 @@ import { codeExecutionService } from '@/services/codeExecutionService';
import { clearUserEncryptionData } from '@/lib/encryption';
import { MonacoThemeProvider } from '@/contexts/MonacoThemeContext';
import MainApp from '@/pages/MainApp';
+import PublicNotePage from '@/pages/PublicNotePage';
function AppContent() {
const { getToken, isSignedIn } = useAuth();
@@ -46,6 +47,7 @@ function AppContent() {
const isSignInPage = window.location.pathname === '/sign-in';
const isSignUpPage = window.location.pathname === '/sign-up';
+ const isPublicNotePage = window.location.pathname.startsWith('/p/');
// Check if user wants to force web version
const urlParams = new URLSearchParams(window.location.search);
@@ -58,10 +60,15 @@ function AppContent() {
localStorage.setItem('forceWebVersion', 'true');
}
- if (isMobileDevice && !isSignedIn && !forceWeb) {
+ if (isMobileDevice && !isSignedIn && !forceWeb && !isPublicNotePage) {
return
+ Changes to your note are automatically synced to the public version + when you save. +
++ Last synced: {new Date(note.publicUpdatedAt).toLocaleString()} +
+ )} ++ Displayed on the public page. Leave blank to publish anonymously. +
++ ⚠️ Warning: Publishing bypasses end-to-end encryption. + An unencrypted copy will be stored on our servers and anyone with the + link can view it. +
+Loading note...
++ {error === 'Note not found' + ? 'This note may have been unpublished or the link is incorrect.' + : 'Something went wrong while loading this note. Please try again later.'} +
+