How I Built My Note Platform Using Markdown + Firebase
The Genesis of My Custom Note Platform
We’ve all been there. You start with Evernote, then switch to Notion, maybe dabble with Obsidian, Roam Research, or even a plain text editor. Each offers something compelling, but none quite hit the sweet spot for my particular workflow. I found myself constantly battling feature bloat, proprietary formats, or sync inconsistencies. What I craved was simplicity, ownership of my data, and real-time accessibility across devices.
My solution? Building my own. This might sound like a significant undertaking, but by leveraging the power of Markdown for content and Firebase for the backend, I created a surprisingly robust and highly personalized note-taking platform that addresses my core needs.
This post will walk you through the “why,” the “how,” and the “what next” of building a note platform with Markdown and Firebase.
Why Markdown and Firebase? A Match Made in Developer Heaven
The choice of technologies wasn’t arbitrary. It stemmed from a desire for efficiency, portability, and scalability without the overhead of managing complex server infrastructure.
Markdown: The Unsung Hero of Text
Markdown is a lightweight markup language that allows you to format plain text. Its beauty lies in its simplicity and ubiquity.
- Future-Proof & Portable: Markdown files are just plain text. This means they are inherently future-proof and can be opened, edited, and rendered by virtually any text editor or compatible application, now and decades from now. No vendor lock-in!
- Focus on Content: The minimal syntax encourages you to focus on writing, not on styling. You write, and the rendering engine handles the presentation.
- Ubiquitous Tooling: From GitHub to Reddit, VS Code to Obsidian, Markdown is everywhere. This familiarity reduces cognitive load.
- Easy to Parse & Render: Libraries exist in every major programming language to convert Markdown into beautiful HTML.
Here’s a quick example of Markdown:
# My Awesome Note
This is a **paragraph** with some *emphasis*.
- Item one
- Item two
- Sub-item
## A Subheading
[Link to Google](https://www.google.com)
Firebase: Your Backend-as-a-Service Powerhouse
Firebase, Google’s mobile and web application development platform, is an absolute game-changer for solo developers or small teams. It takes care of all the tedious backend boilerplate, letting you focus on your application’s unique features.
- Realtime Database / Firestore: This is the heart of a real-time note-taking app. Firestore, Firebase’s NoSQL document database, offers real-time synchronization, offline support, and powerful querying capabilities. Changes made on one device are instantly reflected on others.
- Authentication: Handling user sign-ups, logins, and session management is notoriously tricky. Firebase Auth provides secure, robust authentication out-of-the-box, supporting email/password, social logins (Google, Facebook, etc.), and more.
- Hosting: Deploying your web application is dead simple with Firebase Hosting, complete with SSL certificates and global CDN distribution.
- Scalability: Firebase services automatically scale as your user base grows, so you don’t have to worry about server provisioning or load balancing.
- Cost-Effective: For projects with moderate usage, Firebase offers generous free tiers, making it incredibly affordable to get started.
Architectural Blueprint
My note platform follows a classic client-server architecture, with Firebase acting as the “server.”
- Frontend (Vue.js): A single-page application (SPA) built with Vue.js (though React, Angular, or even vanilla JavaScript would work equally well). This handles:
- User authentication (login/signup).
- Displaying a list of notes.
- Providing a Markdown editor and a real-time previewer.
- Interacting with Firebase Firestore to save, retrieve, update, and delete notes.
- Firebase Firestore: The primary database for storing note content and metadata (title, creation date, last updated date, user ID, tags).
- Firebase Authentication: Manages user accounts, ensuring only authenticated users can access their notes.
- Firebase Hosting: Serves the Vue.js frontend application to the web.
- Optional: Firebase Storage: For handling file uploads (e.g., images embedded in notes).
- Optional: Firebase Cloud Functions: For advanced backend logic, like triggering a search indexer when a note changes (more on this later).
Building It: A Step-by-Step Breakdown
Let’s dive into the practical aspects. For this guide, I’ll use Vue.js as the frontend framework, but the core Firebase interactions remain largely the same regardless of your UI library.
1. Frontend Project Setup (Vue.js)
First, set up your Vue.js project and install the Firebase SDK.
# If you don't have Vue CLI installed
npm install -g @vue/cli
# Create a new Vue project
vue create my-markdown-notes
# Navigate into the project directory
cd my-markdown-notes
# Install Firebase SDK
npm install firebase
2. Firebase Project Initialization
Go to the Firebase Console.
- Click “Add project” and follow the prompts.
- Once created, click the web icon (
</>
) to add a web app to your project. - You’ll be given a Firebase configuration object. Keep this safe and don’t expose it client-side in a public repository without proper environment variable handling for production apps, though for client-side configuration, it’s generally considered safe as it only grants access to configured services with security rules.
Your src/main.js
(or a dedicated firebaseConfig.js
file) might look something like this:
// src/firebaseConfig.js
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
// Initialize Firebase services
const auth = getAuth(app);
const db = getFirestore(app);
export { auth, db };
3. Authentication: Securing Your Notes
Firebase Auth makes user management incredibly straightforward.
- In the Firebase Console, navigate to “Authentication” and enable “Email/Password” and optionally “Google” (or other providers) under the “Sign-in method” tab.
- On your frontend, create components for login and registration.
<!-- src/components/AuthForm.vue -->
<template>
<div>
<input type="email" v-model="email" placeholder="Email" />
<input type="password" v-model="password" placeholder="Password" />
<button @click="signUp">Sign Up</button>
<button @click="signIn">Sign In</button>
<p v-if="error">{{ error }}</p>
</div>
</template>
<script>
import { createUserWithEmailAndPassword, signInWithEmailAndPassword, onAuthStateChanged, signOut } from "firebase/auth";
import { auth } from "@/firebaseConfig"; // Adjust path as needed
export default {
data() {
return {
email: '',
password: '',
error: null,
user: null
};
},
created() {
// Listen for auth state changes
onAuthStateChanged(auth, (user) => {
this.user = user;
if (user) {
console.log("User is logged in:", user.email);
// Optionally redirect or load user-specific data
} else {
console.log("No user is logged in.");
}
});
},
methods: {
async signUp() {
try {
this.error = null;
await createUserWithEmailAndPassword(auth, this.email, this.password);
alert('Account created! You are now logged in.');
} catch (err) {
this.error = err.message;
console.error("Signup error:", err.message);
}
},
async signIn() {
try {
this.error = null;
await signInWithEmailAndPassword(auth, this.email, this.password);
alert('Logged in successfully!');
} catch (err) {
this.error = err.message;
console.error("Signin error:", err.message);
}
},
async signOutUser() {
try {
await signOut(auth);
alert('Logged out.');
} catch (err) {
this.error = err.message;
console.error("Signout error:", err.message);
}
}
}
};
</script>
4. Storing Notes in Firestore
The core of the app: storing Markdown content. I chose a notes
collection in Firestore. Each document represents a single note and includes:
userId
: To link the note to its owner (crucial for security rules).title
: The note’s title (e.g., first line of Markdown).content
: The full Markdown string.createdAt
: Timestamp of creation.updatedAt
: Timestamp of last modification.tags
: An array of strings for categorization (optional).
Firestore Data Model (example document):
// notes/{noteId}
{
"userId": "someFirebaseUserId",
"title": "My First Markdown Note",
"content": "# My First Markdown Note\n\nThis is the *content* of my note.",
"createdAt": "Timestamp(seconds=..., nanoseconds=...)",
"updatedAt": "Timestamp(seconds=..., nanoseconds=...)",
"tags": ["personal", "ideas"]
}
CRUD Operations (within a Vue component or service):
// src/services/noteService.js (conceptual)
import { db, auth } from "@/firebaseConfig";
import { collection, query, where, orderBy, onSnapshot, addDoc, doc, updateDoc, deleteDoc, serverTimestamp } from "firebase/firestore";
export function setupNotesListener(callback) {
if (!auth.currentUser) return; // Ensure user is logged in
const q = query(
collection(db, "notes"),
where("userId", "==", auth.currentUser.uid),
orderBy("updatedAt", "desc")
);
// onSnapshot provides real-time updates
return onSnapshot(q, (snapshot) => {
const notes = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
callback(notes);
}, (error) => {
console.error("Error fetching notes:", error);
});
}
export async function createNote(title, content) {
if (!auth.currentUser) throw new Error("User not authenticated.");
return await addDoc(collection(db, "notes"), {
userId: auth.currentUser.uid,
title,
content,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp(),
});
}
export async function updateNote(noteId, newTitle, newContent) {
if (!auth.currentUser) throw new Error("User not authenticated.");
const noteRef = doc(db, "notes", noteId);
await updateDoc(noteRef, {
title: newTitle,
content: newContent,
updatedAt: serverTimestamp(),
});
}
export async function deleteNote(noteId) {
if (!auth.currentUser) throw new Error("User not authenticated.");
await deleteDoc(doc(db, "notes", noteId));
}
Crucial: Firestore Security Rules
Without proper security rules, any authenticated user could read or write to any note! Firebase security rules are essential.
In your Firebase Console, navigate to “Firestore Database” -> “Rules” tab.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Only authenticated users can access the 'notes' collection
match /notes/{noteId} {
allow read, write: if request.auth.uid != null && request.auth.uid == resource.data.userId;
}
// Note: If you have other collections, define rules for them too.
// For example, if you had a 'users' collection where user profiles are stored:
// match /users/{userId} {
// allow read: if request.auth.uid != null; // Anyone logged in can read user profiles
// allow write: if request.auth.uid == userId; // Only a user can write their own profile
// }
}
}
This rule ensures:
request.auth.uid != null
: Only a logged-in user can perform operations.request.auth.uid == resource.data.userId
: A user can only read or write their own notes (where theuserId
field in the note document matches their authenticated user ID).
5. Markdown Rendering
To display the Markdown content as HTML, you’ll need a Markdown parsing library. marked
(now marked.js
) or markdown-it
are excellent choices.
npm install marked
Then, in your Vue component:
<!-- src/components/NoteEditor.vue -->
<template>
<div class="note-container">
<div class="editor-pane">
<input type="text" v-model="currentNote.title" placeholder="Note Title" @input="debouncedSave" />
<textarea v-model="currentNote.content" @input="debouncedSave"></textarea>
</div>
<div class="preview-pane" v-html="renderedContent"></div>
</div>
</template>
<script>
import { marked } from 'marked';
import { updateNote } from '@/services/noteService'; // Adjust path as needed
import { debounce } from 'lodash'; // npm install lodash
export default {
props: ['note'], // Expects a 'note' object from parent
data() {
return {
currentNote: this.note ? { ...this.note } : { title: '', content: '' },
debouncedSave: null
};
},
created() {
// Debounce the save operation to avoid excessive Firestore writes
this.debouncedSave = debounce(this.saveNoteChanges, 1000); // Save after 1 second of inactivity
},
watch: {
note: {
handler(newNote) {
if (newNote) {
this.currentNote = { ...newNote };
} else {
this.currentNote = { title: '', content: '' };
}
},
deep: true,
immediate: true
}
},
computed: {
renderedContent() {
// Basic markdown rendering. Consider sanitization for user-generated content.
return marked.parse(this.currentNote.content || '');
}
},
methods: {
async saveNoteChanges() {
if (this.currentNote.id) {
// Update existing note
try {
await updateNote(this.currentNote.id, this.currentNote.title, this.currentNote.content);
console.log("Note saved:", this.currentNote.title);
} catch (error) {
console.error("Error saving note:", error);
}
} else {
// Note: For new notes, you'd have a separate "create" action.
// This component focuses on editing an *existing* note.
console.log("This component is for editing, not creating new notes without an ID.");
}
}
}
};
</script>
<style scoped>
.note-container {
display: flex;
height: calc(100vh - 60px); /* Adjust based on header height */
gap: 20px;
}
.editor-pane, .preview-pane {
flex: 1;
padding: 20px;
border: 1px solid #eee;
overflow-y: auto;
}
textarea {
width: 100%;
height: calc(100% - 40px); /* Adjust for title input */
border: none;
resize: none;
font-family: monospace;
}
input[type="text"] {
width: 100%;
padding: 10px;
margin-bottom: 10px;
border: 1px solid #ccc;
}
.preview-pane {
background-color: #f9f9f9;
}
</style>
Note: When rendering user-generated Markdown to HTML, it’s crucial to implement sanitization to prevent Cross-Site Scripting (XSS) attacks. Libraries like dompurify
can help with this: marked.setOptions({ sanitizer: DOMPurify.sanitize });
.
6. Deployment with Firebase Hosting
Once your app is ready, deploying it is a breeze.
# If you don't have Firebase CLI installed
npm install -g firebase-tools
# Initialize Firebase for your project
firebase init
# Follow prompts:
# - Choose "Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys"
# - Select your Firebase project
# - For public directory, enter `dist` (Vue's default build output)
# - Configure as a single-page app: Yes
# - Set up automatic builds and deploys with GitHub: (Your preference)
# Build your Vue app
npm run build
# Deploy to Firebase
firebase deploy
Your app will then be live at a YOUR_PROJECT_ID.web.app
domain!
Challenges and Solutions
Building this platform wasn’t without its nuanced challenges.
Offline Support
Firestore provides excellent offline capabilities out-of-the-box for its data. When a user loses connection, changes are queued locally and synchronized once connectivity is restored. To enhance the overall offline experience, especially for static assets and the app’s shell, I wrapped the application as a Progressive Web App (PWA) using Vue CLI’s PWA plugin.
Full-Text Search
Firestore is fantastic for structured queries, but it doesn’t natively support full-text search across document content. This is a common requirement for a note-taking app. My solutions:
- Client-Side Filtering (Basic): For a small number of notes, you can fetch all notes and filter them client-side using JavaScript
String.prototype.includes()
or regular expressions. This quickly becomes inefficient with many notes. - Third-Party Search Services (Scalable): For robust full-text search, integration with services like Algolia or Elasticsearch (often deployed on Google Cloud or AWS) is the way to go. You’d typically use a Firebase Cloud Function to trigger an update to your search index whenever a note is created, updated, or deleted in Firestore. Firebase also offers Algolia extensions that simplify this.
- Firestore + N-gram Indexing (Manual): A more advanced approach involves creating an N-gram index within Firestore for each note, then querying those n-grams. This can get complex and resource-intensive for very large datasets.
For my initial version, client-side filtering sufficed, but I’ve since explored Algolia for a more scalable solution.
Version Control / Note History
Unlike some robust note apps, Firebase Firestore doesn’t automatically keep versions of your documents. If you want to see previous states of a note, you need to implement this manually.
Note: One approach is to create a subcollection history
under each note document (notes/{noteId}/history/{versionId}
). Each document in history
would be a snapshot of the note’s content
and updatedAt
time. This can lead to increased reads and storage costs if not managed carefully.
Future Enhancements and Ideas
My note platform is a living project. Here are some ideas for future development:
- Rich Text Editor Integration: While I love Markdown, sometimes a WYSIWYG editor is convenient. Integrating something like TinyMCE or Quill.js that outputs Markdown rather than HTML would be ideal.
- Tagging and Categorization Improvements: More sophisticated tag management, tag clouds, and filtering.
- Note Sharing: Securely share notes with other users, perhaps read-only or with collaborative editing features (more complex and would require careful Firestore security rule adjustments).
- Image Uploads: Integrating Firebase Storage to allow users to upload images and embed them directly into their Markdown.
- Mobile App Wrapper: Using Capacitor or Electron to turn the web app into native mobile/desktop applications.
- Dark Mode: A simple, yet highly requested feature for many apps.
Conclusion
Building my own note platform using Markdown and Firebase has been an incredibly rewarding experience. It solidified my understanding of modern web development patterns, exposed me to the power of serverless architectures, and most importantly, provided me with a highly personalized tool that genuinely improves my productivity.
The combination of Markdown’s simplicity and Firebase’s robust backend services allows for rapid development of powerful, real-time applications without the typical complexities of infrastructure management. If you’re looking to scratch a particular itch, learn new skills, and gain full control over your digital tools, I highly recommend embarking on your own custom app development journey. The power to build is truly liberating.
References & Further Reading
- Firebase Documentation: The definitive guide for all Firebase services.
- Vue.js Documentation: For frontend development.
- Markdown Parsers:
- XSS Prevention (DOMPurify):
- Lodash (Debounce):
- Algolia Firebase Extension: