A beautiful, privacy-focused anonymous letter platform with a vintage paper aesthetic. Built with vanilla HTML, CSS, JavaScript, and deployed on Netlify with Google Sheets as the backend.
- No login required - Send messages completely anonymously
- Session tracking - Unique session ID per device (no personal data)
- Character counter - Real-time counter with warnings
- Auto-save drafts - Messages saved to localStorage
- Spam protection - Honeypot field catches bots
- Vintage paper aesthetic - Torn edges, grid pattern, paper texture
- Fully responsive - Works perfectly on mobile and desktop
- Minimal UI - Clean, distraction-free interface
- Custom postage stamp - Link to create your own instance
- Authentication:
- Static PIN (set by you, required)
- Time-based PIN (optional second factor - only active if you set
TIME_PIN_ALGORITHM)
- Session management - Auto-logout after 30 minutes
- Protected data access - All API calls require authentication
- Statistics - Total messages, today's count, unique sessions, weekly stats
- Search & Filter - Find messages by content or session ID
- Sort options - Newest/oldest first
- Time filters - Today, this week, this month, or all
- Real-time refresh - Reload messages anytime
- Clean table view - Easy to read and manage
- GitHub account (free)
- Google account (for Google Sheets)
- Netlify account (free, can sign up with GitHub)
Click the "Fork" button at the top right of this page to create your own copy.
- Create a new Google Sheet
- Go to Extensions β Apps Script
- Delete any existing code
- Paste the code from
google-apps-script.js(see below) - Save and deploy as Web App (see detailed instructions below)
- Go to Netlify
- Click Add new site β Import an existing project
- Connect your GitHub account
- Select your forked repository
- Click Deploy
In Netlify, go to Site configuration β Environment variables and add:
DASHBOARD_PIN = <yourPassword>
GSCRIPT_URL = <https://script.google.com/macros/s/.../exec>
These two are the only environment variables required for the site to work. Everything else (like the time-PIN algorithm) has a working default baked into the code.
Edit js/config.js:
const CONFIG = {
username: "your_username",
siteName: "YOUR SITE NAME",
siteTagline: "Your tagline here",
maxMessageLength: 2000,
onboardingUrl: "/onboard.html"
};Your site is live! Share your URL and start receiving anonymous messages.
urochithi/
βββ index.html # Main landing page
βββ dashboard.html # Protected admin dashboard
βββ onboard.html # Setup guide for new users
βββ css/
β βββ styles.css # All styles (paper aesthetic)
βββ js/
β βββ config.js # Configuration (EDIT THIS!)
β βββ main.js # Main functionality
βββ netlify/
β βββ functions/
β βββ submit.js # Submit messages
β βββ get-messages.js # Fetch messages (auth required)
β βββ verify-static-pin.js # Step 1 authentication
β βββ verify-time-pin.js # Step 2 authentication
βββ README.md # This file
-
Open Apps Script Editor
- In your Google Sheet: Extensions β Apps Script
-
Paste This Code
// ============================================
// GOOGLE APPS SCRIPT - READ MESSAGES
// ============================================
// Add this function to your existing Apps Script
// This allows the dashboard to read messages
function doGet(e) {
try {
const sheet = SpreadsheetApp.getActiveSheet();
const lastRow = sheet.getLastRow();
// If no data, return empty array
if (lastRow <= 1) {
return ContentService
.createTextOutput(JSON.stringify({ messages: [] }))
.setMimeType(ContentService.MimeType.JSON);
}
// Get all data (skip header row)
const range = sheet.getRange(2, 1, lastRow - 1, 3);
const values = range.getValues();
// Transform to JSON
const messages = values.map(row => ({
timestamp: row[0] ? new Date(row[0]).toISOString() : new Date().toISOString(),
message: row[1] || "",
sessionId: row[2] || "unknown"
}));
return ContentService
.createTextOutput(JSON.stringify({ messages: messages }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
Logger.log("Error in doGet: " + error.toString());
return ContentService
.createTextOutput(JSON.stringify({
messages: [],
error: error.toString()
}))
.setMimeType(ContentService.MimeType.JSON);
}
}
// Keep your existing doPost function below
function doPost(e) {
try {
const data = JSON.parse(e.postData.contents);
const sheet = SpreadsheetApp.getActiveSheet();
if (sheet.getLastRow() === 0) {
sheet.appendRow(["Timestamp", "Message", "Session ID"]);
const headerRange = sheet.getRange(1, 1, 1, 3);
headerRange.setFontWeight("bold");
headerRange.setBackground("#f0ede5");
headerRange.setHorizontalAlignment("center");
sheet.setColumnWidth(1, 180);
sheet.setColumnWidth(2, 400);
sheet.setColumnWidth(3, 200);
}
const row = [
data.timestamp || new Date().toISOString(),
data.message || "",
data.sessionId || "N/A"
];
sheet.appendRow(row);
const lastRow = sheet.getLastRow();
sheet.getRange(lastRow, 2).setWrap(true);
if (lastRow % 2 === 0) {
sheet.getRange(lastRow, 1, 1, 3).setBackground("#faf8f3");
}
return ContentService
.createTextOutput(JSON.stringify({ ok: true }))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
Logger.log("Error in doPost: " + error.toString());
return ContentService
.createTextOutput(JSON.stringify({
ok: false,
error: error.toString()
}))
.setMimeType(ContentService.MimeType.JSON);
}
}- Deploy as Web App
- Click Deploy β New deployment
- Type: Web app
- Execute as: Me
- Who has access: Anyone
- Click Deploy
- Copy the deployment URL - you'll need this!
A password you set in the DASHBOARD_PIN environment variable.
Example: mySecretPassword123
If you set a TIME_PIN_ALGORITHM environment variable, login becomes two-factor: after the static PIN, you'll also be asked for a code that changes every minute, calculated from the current UTC time.
If TIME_PIN_ALGORITHM is not set, this step is skipped entirely - logging in with just the static PIN is enough.
Example Formula: (hour Γ 7) + (minute % 10)
Example at 14:42 UTC:
(14 Γ 7) + (42 % 10)
= 98 + 4
= 102
Enable the time-based PIN step by setting a TIME_PIN_ALGORITHM environment variable:
Simple:
TIME_PIN_ALGORITHM = (hour + minute)
Medium:
TIME_PIN_ALGORITHM = (hour * 7) + (minute % 10)
Complex:
TIME_PIN_ALGORITHM = (hour * hour) + (minute * 3)
Variables available:
hour- Current UTC hour (0-23)minute- Current UTC minute (0-59)- Operators:
+,-,*,/,%,()
Set these in Netlify β Site configuration β Environment variables:
| Variable | Required | Description | Example |
|---|---|---|---|
DASHBOARD_PIN |
Yes | Static password for dashboard | mySecretPass123 |
GSCRIPT_URL |
Yes | Google Apps Script deployment URL | https://script.google.com/... |
TIME_PIN_ALGORITHM |
No | Formula for time-based code. If unset, the time-PIN step is skipped entirely (single-factor login) | (hour * 7) + (minute % 10) |
RECAPTCHA_SECRET_KEY |
No | Server-side key for reCAPTCHA v3 verification on dashboard login. If unset, reCAPTCHA verification is skipped automatically - it doesn't block login | 6Lc... |
Only DASHBOARD_PIN and GSCRIPT_URL are required for the app to run. Everything else is skipped gracefully when not set.
- Timestamp - When the message was sent
- Message - The actual letter content
- Session ID - Unique identifier per device/browser (generated client-side)
- β Names or emails
- β IP addresses
- β GPS coordinates
- β Browser fingerprints
- β Tracking cookies
- β Any personal information
- Generated once per device/browser
- Stored in localStorage
- Format:
timestamp-random(e.g.,lq8x7k9m-a3b4c5d6e) - Helps identify messages from the same sender
- No way to trace back to actual identity
Edit css/styles.css and replace these hex codes:
#5d4037- Dark brown (primary text)#8d6e63- Medium brown (buttons, borders)#f4f1e8- Light beige (body background)#faf8f3- Off-white (paper background)
In index.html, update the Google Fonts link:
<link href="https://fonts.googleapis.com/css2?family=YOUR_FONT&display=swap" rel="stylesheet">Then update font-family in css/styles.css.
Edit js/config.js:
maxMessageLength: 2000 // Change to your desired limitAlso update in netlify/functions/submit.js:
if (!data.message || data.message.length > 2000) {- β Two-factor authentication for dashboard
- β Time-based PIN changes every minute
- β 3-minute window for clock sync issues
- β Session timeout (30 minutes of inactivity)
- β Server-side PIN verification
- β Honeypot spam protection
- β Input validation and sanitization
- β HTTPS by default (Netlify)
- β No sensitive data in client-side code
Check:
GSCRIPT_URLis set correctly in Netlify- Apps Script is deployed with "Anyone" access
- Apps Script has both
doGetanddoPostfunctions - Browser console for error messages (F12)
Fix:
- Redeploy Apps Script
- Update
GSCRIPT_URLin Netlify - Trigger new deployment in Netlify
Static PIN error:
- Verify
DASHBOARD_PINmatches exactly (case-sensitive) - Check environment variable is set in Netlify
Time-based PIN error:
- Use the UTC time shown on screen
- Double-check your calculation
- Make sure
TIME_PIN_ALGORITHMis set correctly - Try the next minute's code (3-minute window)
Cause: 30-minute inactivity timeout
Fix: Just log in again with both PINs
Fully responsive design with:
- Touch-friendly buttons and inputs
- Optimized layouts for small screens
- Readable fonts on mobile
- Adjusted navigation for mobile
- Full functionality on all devices
Contributions are welcome! Here's how:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
# Clone your fork
git clone https://github.com/YOUR_USERNAME/urochithi.git
cd urochithi
# Make changes
# Test locally (use a local server for testing)
python -m http.server 8000
# or
npx serve
# Commit and push
git add .
git commit -m "Your changes"
git pushMIT License - feel free to use for personal or commercial projects!
MIT License
Copyright (c) 2024 Urochithi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- Inspired by anonymous messaging platforms like NGL and Tellonym
- Font: Special Elite by Google Fonts
- Hosted on Netlify
- Data storage: Google Sheets
- π Bug reports: Open an issue
- π‘ Feature requests: Start a discussion
- π§ Contact: Create an issue or discussion
- π Documentation: See
/onboard.htmlon your deployed site
- Email notifications for new messages
- Reply functionality (optional for senders)
- Message categories/tags
- Export messages as PDF
- Custom domain support guide
- Dark mode toggle
- Multiple language support
- Analytics dashboard enhancements
- Message moderation tools
- Batch operations in dashboard
If you found this helpful:
- β Star this repository
- π΄ Fork it for your own project
- π£ Share with friends
- π Report bugs or suggest features
- π Consider sponsoring
Live Demo β’ Report Bug β’ Request Feature
Made by @hello2himel