Originly - The Daily Capture Loop

Build a mobile-first app that cryptographically proves when you had an idea.

Introduction

30 Second Summary

You have a brilliant idea in the shower, scribble it on a napkin at lunch, or whisper it into a voice memo at 2am. But how do you prove you had that idea on that exact day without sharing it with anyone?

In this project, you will build a mobile-first web app called Originly that captures text ideas and generates cryptographic proof-of-existence for each one. Every time you save an idea, the app creates a unique digital fingerprint and timestamps it server-side, proving you had that thought at that moment.

What You'll Build

Originly is a Next.js 16 app powered by Firebase that hashes ideas with the Web Crypto API and stores them with server timestamps in Firestore as cryptographic proof-of-existence.

By the end of this project, you'll have:

  • A working Next.js 16 app with Google sign-in via Firebase, deployed live on Vercel.
  • A capture screen that generates SHA-256 fingerprints using the browser's Web Crypto API for tamper-evident proof.
  • A personal vault protected by Firestore security rules, ensuring only the authenticated owner can read their ideas.
  • Secret Mission: An integrity check feature that cryptographically verifies whether pasted text matches a previously captured idea's hash.

Are there any prerequisites?

You should be comfortable with React and Next.js basics (components, hooks, and routing). You will also need a Firebase account, a GitHub account, and a Vercel account (free to sign up using your GitHub login).

Set Up the Project

Before you can capture ideas and generate cryptographic proof, you need a working development environment. This means scaffolding the app, connecting it to a backend for authentication and storage, and writing the utility functions that power the proof-of-existence feature.

In this step, you will create a Next.js project, set up a Firebase project for authentication and data storage, and write two utility files: one to initialize Firebase and one to hash text using SHA-256.

In this step, get ready to:
  • Scaffold a Next.js 16 project with JavaScript, Tailwind CSS, App Router, and a src/ directory.
  • Create a Firebase project with Google authentication and a Firestore database.
  • Write the Firebase initialization file and the SHA-256 hashing utility.
Scaffold the Next.js project and install Firebase

You will use the create-next-app CLI to scaffold a new Next.js project, then install the Firebase SDK as a dependency.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your Next.js project by running this command:
npx create-next-app@latest originly

What happens next?

The CLI will first ask whether you want to use the recommended Next.js defaults or customize settings. Since the defaults use TypeScript (and this project uses JavaScript), you need to select No, customize settings.

If npm asks you to confirm installation of the create-next-app package, type y and press Enter.

  • When asked Would you like to use the recommended Next.js defaults?, select No, customize settings.
  • Select No for TypeScript.
  • Select ESLint for the linter.
  • Select No for React Compiler.
  • Select Yes for Tailwind CSS.
  • Select Yes for the src/ directory.
  • Select Yes for App Router.
  • Select No for customizing the import alias.
  • Select Yes for including AGENTS.md.

After the prompts, the CLI creates your project folder and installs all dependencies.

  • Move into the project folder and install the Firebase SDK by running these commands:
cd originly
npm install firebase@12.15.0

What does this install?

The firebase package includes Firebase Authentication, Firestore, and other Firebase services. Pinning version 12.15.0 ensures everyone on this project uses the same tested version.

  • Confirm the installation succeeded by running:
ls node_modules/firebase

You should see a list of folders inside the Firebase package, confirming it installed correctly.

Command not found or install failed?

Make sure you ran cd originly before the install command. The npm install command must run inside your project folder where package.json lives.

If npx or npm is not recognized, your Node.js installation may not be on your system PATH. Reinstall Node.js from nodejs.org and reopen your terminal.

help me fix my npm or npx installation error

Create a Firebase project with authentication and Firestore

Firebase provides the backend services for Originly: Google sign-in for authentication and Firestore for storing ideas. You will configure both in the Firebase Console.

  • Open console.firebase.google.com in your browser.
  • Click Create a project (or Add project).
  • Enter Originly as the project name.
  • Accept the defaults for Google Analytics (you can disable it for this project).
  • Click Create project and wait for it to finish.

Once the project is created, you need to enable Google sign-in.

  • In the left sidebar, click Build, then click Authentication.
  • Click Get started.
  • Under Sign-in method, click Google.
  • Toggle the Enable switch on.
  • Select a support email from the dropdown.
  • Click Save.

Now create the Firestore database where ideas will be stored.

  • In the left sidebar, click Build, then click Firestore Database.
  • Click Create database.
  • Select Start in test mode.
  • Choose a Cloud Firestore location close to you.
  • Click Create.

Why test mode?

Test mode allows any authenticated request to read and write data. This lets you build and test without worrying about permissions yet. You will lock this down with proper security rules in Step 5.

Finally, register a web app so Firebase gives you the configuration values your code needs.

  • Click the gear icon next to Project Overview in the left sidebar.
  • Click Project settings.
  • Scroll down to Your apps and click the web icon (</>).
  • Enter Originly as the app nickname.
  • Click Register app.
  • You will see a firebaseConfig object on screen. Keep this page open. You will copy these values in the next substep.
Create the Firebase configuration and hashing utility files

Your app needs two utility files: one to connect to Firebase and one to generate SHA-256 hashes. You will also create an environment file to store your Firebase configuration values.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the originly folder, and click Open.

First, create an environment file to store your Firebase config values securely.

  • In the VS Code Explorer sidebar, right-click in the root of the originly folder.
  • Select New File and name it .env.local.
  • Paste the following into .env.local, replacing each placeholder with the matching value from your Firebase config object:
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key-here
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project-id.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project-id.firebasestorage.app
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=your-sender-id
NEXT_PUBLIC_FIREBASE_APP_ID=your-app-id

Where do I find these values?

Go back to the Firebase Console tab you left open. The firebaseConfig object displays each value next to its key. Copy each value (without quotes) into the corresponding line in .env.local.

The NEXT_PUBLIC_ prefix tells Next.js to expose these variables to the browser. Firebase web config values are designed to be public. Security comes from Firestore rules, not from hiding the config.

  • Save .env.local.

Now create the Firebase initialization file.

  • In the Explorer sidebar, expand the src folder.
  • Right-click on src and select New Folder. Name it lib.
  • Right-click on the new lib folder and select New File. Name it firebase.js.
  • Paste the following code into firebase.js:
import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);

What does this code do?

This file initializes Firebase using the config values from your environment variables. It then creates and exports two services your app will use throughout the project.

  • auth handles user sign-in and sign-out with Google.
  • db is the Firestore database connection for storing and retrieving ideas.
  • Save firebase.js.

Now create the hashing utility that generates cryptographic fingerprints of ideas.

  • Right-click on the lib folder and select New File. Name it hash.js.
  • Paste the following code into hash.js:
export async function generateHash(text) {
  const encoder = new TextEncoder();
  const data = encoder.encode(text);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return hashHex;
}

What does this code do?

This function takes any text and produces a unique 64-character hexadecimal fingerprint using the browser's Web Crypto API.

  • TextEncoder converts the string to bytes that the hashing algorithm can process.
  • crypto.subtle.digest("SHA-256", data) runs the SHA-256 algorithm and returns raw bytes.
  • The map and join convert those bytes into a readable hex string (like a3f2...9b1c).

The same input always produces the same output. This is what makes it useful as proof-of-existence. If even one character changes, the hash is completely different.

  • Save hash.js.

✔️ Awesome, I've got everything!

Great. Double check you have saved both src/lib/firebase.js and src/lib/hash.js.

ⓧ I'd like to double check the full code

Here is src/lib/firebase.js:

import { initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
export const auth = getAuth(app);
export const db = getFirestore(app);

Here is src/lib/hash.js:

export async function generateHash(text) {
  const encoder = new TextEncoder();
  const data = encoder.encode(text);
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hashHex = hashArray
    .map((b) => b.toString(16).padStart(2, "0"))
    .join("");
  return hashHex;
}

Now verify everything works by starting the development server.

  • Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key).
  • Start the dev server by running:
npm run dev
  • Open http://localhost:3000 in your browser.

You should see the default Next.js welcome page. This confirms your project is scaffolded correctly and the dev server is running.

Page not loading?

Check that port 3000 is not already in use by another application. If it is, Next.js will suggest an alternative port in the terminal output.

If you see a Firebase-related error in the terminal, double check that your .env.local file is in the root of the originly folder (not inside src/).

help me troubleshoot my Next.js dev server not starting

Your development environment is fully set up. Next, you will build the authentication flow so users can sign in with their Google account.

Build Authentication

Your project skeleton is ready with Firebase initialized and the hashing utility in place. Now you need a way to identify who is capturing ideas.

In this step, you will build the full authentication flow using Firebase Auth and Google sign-in. This ensures every idea saved to Firestore is tied to a specific user, which is critical for the security rules you will add later.

In this step, get ready to:
  • Create an AuthContext that provides user state across the app.
  • Build a login page and a root page that routes users correctly.
  • Build a protected dashboard page shell with sign-out functionality.
Create the AuthContext and wire it into the layout

Your app needs to know whether a user is signed in on every page. React Context is the standard way to share state like this across components without passing props down manually.

  • In VS Code, create a new folder called context inside the src directory.
  • Create a new file called AuthContext.js inside src/context/ and paste the following code:
"use client";

import { createContext, useContext, useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "@/lib/firebase";

// Create a context to hold the current user state
const AuthContext = createContext({});

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Listen for auth state changes (sign in, sign out)
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
    // Clean up the listener when the component unmounts
    return () => unsubscribe();
  }, []);

  return (
    <AuthContext.Provider value={{ user, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

// Custom hook for easy access to auth state
export const useAuth = () => useContext(AuthContext);

What does this code do?

This file sets up a React Context that listens to Firebase's authentication state in real time.

  • onAuthStateChanged fires a callback whenever the user signs in or out. It also fires once on page load to check if a session already exists.
  • The loading state starts as true and flips to false once Firebase has determined the auth state. This prevents pages from flashing incorrect content before the check completes.
  • The useAuth hook gives any component quick access to user and loading without importing the context directly.

Now you need to wrap the entire app with this provider so every page can access the auth state.

  • Open src/app/layout.js and replace its entire contents with:
import "./globals.css";
import { AuthProvider } from "@/context/AuthContext";

export const metadata = {
  title: "Originly",
  description: "Capture and prove ownership of your ideas",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className="bg-gray-50 min-h-screen">
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}

What does this code do?

The AuthProvider wraps {children} so that every page rendered inside the layout automatically has access to the current user state via the useAuth hook.

The metadata export sets the page title and description for the browser tab and SEO.

  • Save both files.

Seeing a module not found error?

  • Make sure the file path is exactly src/context/AuthContext.js. The import uses @/context/AuthContext which maps to the src/ directory.
  • Check that the folder is named context (not contexts) and the file is named AuthContext.js with a capital A and capital C.

Still stuck? Help me fix this module not found error in my AuthContext import.

Build the login page and root page redirector

With the auth state available app-wide, you can now build pages that react to it. The login page shows a Google sign-in button, and the root page redirects users to the right place based on whether they are authenticated.

  • Create a new folder called login inside src/app/.
  • Create a new file called page.js inside src/app/login/ and paste the following code:
"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { auth } from "@/lib/firebase";

export default function LoginPage() {
  const { user, loading } = useAuth();
  const router = useRouter();

  // If already signed in, redirect to dashboard
  useEffect(() => {
    if (!loading && user) {
      router.push("/dashboard");
    }
  }, [user, loading, router]);

  // Trigger Google sign-in popup
  const handleGoogleSignIn = async () => {
    const provider = new GoogleAuthProvider();
    try {
      await signInWithPopup(auth, provider);
    } catch (error) {
      console.error("Sign in error:", error);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen px-4">
      <div className="w-full max-w-sm text-center">
        <h1 className="text-3xl font-bold text-gray-900 mb-2">Originly</h1>
        <p className="text-gray-600 mb-8">
          Capture your ideas. Prove they are yours.
        </p>
        <button
          onClick={handleGoogleSignIn}
          className="w-full bg-gray-900 text-white py-3 px-4 rounded-lg font-medium hover:bg-gray-800 transition-colors"
        >
          Sign in with Google
        </button>
      </div>
    </div>
  );
}

What does this code do?

  • signInWithPopup opens a Google sign-in window. Once the user completes sign-in, onAuthStateChanged in the AuthContext fires automatically and updates the user state.
  • The useEffect at the top checks if a user is already signed in. If so, it redirects them to /dashboard so they never see the login page unnecessarily.
  • GoogleAuthProvider tells Firebase to use Google as the identity provider for this sign-in attempt.

Next, you need the root page to act as a router that sends visitors to the right place.

  • Open src/app/page.js and replace its entire contents with:
"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function Home() {
  const { user, loading } = useAuth();
  const router = useRouter();

  // Redirect based on auth state
  useEffect(() => {
    if (!loading) {
      if (user) {
        router.push("/dashboard");
      } else {
        router.push("/login");
      }
    }
  }, [user, loading, router]);

  return (
    <div className="flex items-center justify-center min-h-screen">
      <p className="text-gray-500">Loading...</p>
    </div>
  );
}

What does this code do?

This page is the entry point at /. It shows a loading message while Firebase determines whether the user has an active session. Once the check completes, it redirects to /dashboard (signed in) or /login (not signed in).

  • Save both files.

Popup blocked or not appearing?

  • Some browsers block popups by default. Check for a popup-blocked icon in the address bar and allow popups for localhost.
  • Make sure you enabled the Google sign-in provider in the Firebase Console under Authentication > Sign-in method. If it is not enabled, you will see an auth/operation-not-allowed error in the browser console.
  • Confirm that localhost appears in your Firebase project's authorized domains list under Authentication > Settings > Authorized domains.

Still stuck? Help me debug why my Google sign-in popup is not working in my Next.js app with Firebase Auth.

✔️ Awesome, I've got everything!

Great. Make sure you have saved both src/app/login/page.js and src/app/page.js.

ⓧ I'd like to double check the full code

Here is the full src/app/login/page.js:

"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { auth } from "@/lib/firebase";

export default function LoginPage() {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && user) {
      router.push("/dashboard");
    }
  }, [user, loading, router]);

  const handleGoogleSignIn = async () => {
    const provider = new GoogleAuthProvider();
    try {
      await signInWithPopup(auth, provider);
    } catch (error) {
      console.error("Sign in error:", error);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen px-4">
      <div className="w-full max-w-sm text-center">
        <h1 className="text-3xl font-bold text-gray-900 mb-2">Originly</h1>
        <p className="text-gray-600 mb-8">
          Capture your ideas. Prove they are yours.
        </p>
        <button
          onClick={handleGoogleSignIn}
          className="w-full bg-gray-900 text-white py-3 px-4 rounded-lg font-medium hover:bg-gray-800 transition-colors"
        >
          Sign in with Google
        </button>
      </div>
    </div>
  );
}

Here is the full src/app/page.js:

"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect } from "react";

export default function Home() {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading) {
      if (user) {
        router.push("/dashboard");
      } else {
        router.push("/login");
      }
    }
  }, [user, loading, router]);

  return (
    <div className="flex items-center justify-center min-h-screen">
      <p className="text-gray-500">Loading...</p>
    </div>
  );
}
Build the protected dashboard and test the full auth flow

The dashboard is where authenticated users will land. For now, it is a simple shell that confirms the user's identity and provides a sign-out button.

  • Create a new folder called dashboard inside src/app/.
  • Create a new file called page.js inside src/app/dashboard/ and paste the following code:
"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { signOut } from "firebase/auth";
import { auth } from "@/lib/firebase";

export default function DashboardPage() {
  const { user, loading } = useAuth();
  const router = useRouter();

  // Redirect to login if not authenticated
  useEffect(() => {
    if (!loading && !user) {
      router.push("/login");
    }
  }, [user, loading, router]);

  // Sign out and let the auth state change handle the redirect
  const handleSignOut = async () => {
    await signOut(auth);
  };

  // Show loading state while checking auth
  if (loading || !user) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <p className="text-gray-500">Loading...</p>
      </div>
    );
  }

  return (
    <div className="min-h-screen pb-8">
      <header className="sticky top-0 bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between z-10">
        <h1 className="text-lg font-bold text-gray-900">Originly</h1>
        <button
          onClick={handleSignOut}
          className="text-sm text-gray-500 hover:text-gray-900"
        >
          Sign out
        </button>
      </header>
      <main className="max-w-lg mx-auto px-4 pt-6">
        <p className="text-gray-700">Welcome, {user.displayName}</p>
      </main>
    </div>
  );
}

What does this code do?

  • The useEffect guard redirects unauthenticated visitors back to /login. This protects the dashboard from being viewed without signing in.
  • signOut(auth) clears the Firebase session. The onAuthStateChanged listener in AuthContext picks up the change and sets user to null, which triggers the redirect.
  • user.displayName comes from the Google account. It confirms the auth flow is working end to end.
  • Save src/app/dashboard/page.js.
  • Return to your browser at http://localhost:3000. You should be redirected to the login page.
  • Click Sign in with Google and complete the Google sign-in flow in the popup.
  • After signing in, you should land on the dashboard and see "Welcome," followed by your Google display name.
  • Click Sign out in the top right. You should be redirected back to the login page.

Not redirecting after sign-in?

  • Check your browser console for errors. A common cause is mismatched Firebase config values in your .env.local file.
  • Make sure your dev server is still running. If you stopped it, start it again with npm run dev.
  • Confirm that AuthProvider wraps {children} in src/app/layout.js. Without it, useAuth() returns undefined values.

Still stuck? Help me debug why my dashboard redirect is not working after Google sign-in.

✔️ Awesome, I've got everything!

Great. Make sure all files are saved and your auth flow works end to end.

ⓧ I'd like to double check the full code

Here is the full src/context/AuthContext.js:

"use client";

import { createContext, useContext, useEffect, useState } from "react";
import { onAuthStateChanged } from "firebase/auth";
import { auth } from "@/lib/firebase";

const AuthContext = createContext({});

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
    return () => unsubscribe();
  }, []);

  return (
    <AuthContext.Provider value={{ user, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

Here is the full src/app/layout.js:

import "./globals.css";
import { AuthProvider } from "@/context/AuthContext";

export const metadata = {
  title: "Originly",
  description: "Capture and prove ownership of your ideas",
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body className="bg-gray-50 min-h-screen">
        <AuthProvider>{children}</AuthProvider>
      </body>
    </html>
  );
}

Here is the full src/app/dashboard/page.js:

"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { signOut } from "firebase/auth";
import { auth } from "@/lib/firebase";

export default function DashboardPage() {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) {
      router.push("/login");
    }
  }, [user, loading, router]);

  const handleSignOut = async () => {
    await signOut(auth);
  };

  if (loading || !user) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <p className="text-gray-500">Loading...</p>
      </div>
    );
  }

  return (
    <div className="min-h-screen pb-8">
      <header className="sticky top-0 bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between z-10">
        <h1 className="text-lg font-bold text-gray-900">Originly</h1>
        <button
          onClick={handleSignOut}
          className="text-sm text-gray-500 hover:text-gray-900"
        >
          Sign out
        </button>
      </header>
      <main className="max-w-lg mx-auto px-4 pt-6">
        <p className="text-gray-700">Welcome, {user.displayName}</p>
      </main>
    </div>
  );
}

Your authentication is fully wired up. Next up, you will build the capture screen that hashes ideas and stores them with cryptographic proof in Firestore.

Build the Capture Screen

Your authentication flow is working. Users can sign in, see the dashboard, and sign out. Now you need the core feature that makes Originly valuable: capturing ideas with cryptographic proof.

In this step, you will build a capture form that takes a text idea, generates a SHA-256 fingerprint of it using the browser's Web Crypto API, and saves everything to Firestore with a server-generated timestamp. The hash proves what you wrote. The timestamp proves when you wrote it.

In this step, get ready to:
  • Build a CaptureForm component that hashes ideas with SHA-256.
  • Save ideas to Firestore with cryptographic proof and a server timestamp.
  • Wire the form into the dashboard and verify it works in the Firebase Console.
Build the CaptureForm component

The CaptureForm handles three responsibilities: collecting the text from the user, hashing it into a unique fingerprint, and writing the result to Firestore. Each captured idea becomes a document with the original content, its SHA-256 hash, and the exact time the server recorded it.

  • Create a new folder called components inside your src directory.
  • Create a new file called CaptureForm.js inside src/components/ and add the following code:
"use client";

import { useState } from "react";
import { collection, addDoc, serverTimestamp } from "firebase/firestore";
import { db } from "@/lib/firebase";
import { useAuth } from "@/context/AuthContext";
import { generateHash } from "@/lib/hash";

export default function CaptureForm() {
  const [content, setContent] = useState("");
  const [saving, setSaving] = useState(false);
  const { user } = useAuth();

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!content.trim() || saving) return;

    setSaving(true);
    try {
      // Generate a SHA-256 fingerprint of the idea
      const hash = await generateHash(content.trim());
      // Save the idea, its hash, and a server timestamp to Firestore
      await addDoc(collection(db, "users", user.uid, "ideas"), {
        content: content.trim(),
        hash: hash,
        createdAt: serverTimestamp(),
      });
      setContent("");
    } catch (error) {
      console.error("Error saving idea:", error);
    }
    setSaving(false);
  };

  return (
    <form onSubmit={handleSubmit} className="w-full">
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="What's your idea?"
        className="w-full p-4 border border-gray-200 rounded-lg resize-none focus:outline-none focus:ring-2 focus:ring-gray-900 min-h-[120px] text-gray-900 bg-white"
      />
      <button
        type="submit"
        disabled={!content.trim() || saving}
        className="mt-3 w-full bg-gray-900 text-white py-3 px-4 rounded-lg font-medium hover:bg-gray-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
      >
        {saving ? "Capturing..." : "Capture Idea"}
      </button>
    </form>
  );
}

What does this code do?

  • generateHash(content.trim()) calls the hash.js utility you created in Step 1. It uses the Web Crypto API's crypto.subtle.digest("SHA-256", data) to produce a 64-character hex string that uniquely represents the text.
  • addDoc(collection(db, "users", user.uid, "ideas"), {...}) saves the idea to a subcollection at the path users/{userId}/ideas/{auto-id}. Each document gets three fields: content, hash, and createdAt.
  • serverTimestamp() tells Firestore to generate the timestamp on its servers, not the user's browser. This means the time cannot be faked by the client.
  • setContent("") clears the textarea after a successful save so the form is ready for the next idea.

Why serverTimestamp instead of Date.now()?

Client-side timestamps (like Date.now()) come from the user's device. A user could change their system clock to fake when an idea was captured. Firestore's serverTimestamp() is generated on Google's servers, making it a more credible proof of when the idea was recorded.

  • Save src/components/CaptureForm.js.

Seeing import errors in your editor?

If your editor shows red underlines on the imports, make sure the file is saved inside src/components/ (not src/app/). Also confirm that src/lib/hash.js and src/lib/firebase.js both exist from Step 1.

Help me fix import errors in my CaptureForm component.

Add the CaptureForm to the dashboard

The component exists but is not rendered anywhere yet. You need to import it into the dashboard page and place it where the welcome message currently sits.

  • Open src/app/dashboard/page.js in VS Code.
  • Find this import line near the top of the file:
import { auth } from "@/lib/firebase";
  • Replace it with this line to also import db:
import { auth, db } from "@/lib/firebase";
  • Add this new import directly below the Firebase import:
import CaptureForm from "@/components/CaptureForm";
  • Now find the <main> section in the return statement. Replace it with this updated version:
      <main className="max-w-lg mx-auto px-4 pt-6 space-y-6">
        <section>
          <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">Capture</h2>
          <CaptureForm />
        </section>
      </main>

What's happening here?

You replaced the static welcome message with the CaptureForm component wrapped in a <section>. The space-y-6 class on <main> adds vertical spacing between sections. You will add more sections in the next step.

  • Save src/app/dashboard/page.js.
  • Switch to your browser and refresh the dashboard. You should see a textarea with the placeholder text "What's your idea?" and a "Capture Idea" button below it.

Don't see the capture form?

Make sure the file path for your CaptureForm is exactly src/components/CaptureForm.js. The import statement uses a capital C in CaptureForm. If the filename does not match, the import fails silently.

Help me debug why my CaptureForm is not showing on the dashboard.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the full src/app/dashboard/page.js at this point:

"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { signOut } from "firebase/auth";
import { auth, db } from "@/lib/firebase";
import CaptureForm from "@/components/CaptureForm";

export default function DashboardPage() {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) {
      router.push("/login");
    }
  }, [user, loading, router]);

  const handleSignOut = async () => {
    await signOut(auth);
  };

  if (loading || !user) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <p className="text-gray-500">Loading...</p>
      </div>
    );
  }

  return (
    <div className="min-h-screen pb-8">
      <header className="sticky top-0 bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between z-10">
        <h1 className="text-lg font-bold text-gray-900">Originly</h1>
        <button
          onClick={handleSignOut}
          className="text-sm text-gray-500 hover:text-gray-900"
        >
          Sign out
        </button>
      </header>
      <main className="max-w-lg mx-auto px-4 pt-6 space-y-6">
        <section>
          <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">Capture</h2>
          <CaptureForm />
        </section>
      </main>
    </div>
  );
}
Test the capture flow in the Firebase Console

The form is live. Now you need to confirm that submitting an idea actually writes the correct data to Firestore with all three fields.

  • In your browser, type a test idea into the textarea (for example, "My first captured idea").
  • Click Capture Idea.
  • The button should briefly show "Capturing..." and then the textarea should clear itself.
  • Open the Firebase Console in a new browser tab.
  • Click Firestore Database in the left sidebar.
  • Expand the users collection, then click your user ID, then expand the ideas subcollection.
  • Click on the document that appeared. You should see three fields:
  • content containing your exact text.
  • hash containing a 64-character hexadecimal string (the SHA-256 fingerprint).
  • createdAt containing a server-generated timestamp.

Why is the hash 64 characters?

SHA-256 produces a 256-bit output. When represented as hexadecimal (where each character encodes 4 bits), that gives exactly 64 characters. This fingerprint is unique to your exact text. Even changing a single space would produce a completely different hash.

Don't see your document in Firestore?

Check your browser's developer console for errors. If you see a permissions error, confirm that your Firestore is still in test mode (you will lock it down in Step 5). Also make sure you are signed into the app before clicking Capture Idea.

Help me debug why my idea is not saving to Firestore.

Your capture loop is working. Ideas go in, get fingerprinted, and land in Firestore with tamper-evident proof. Next up, you will build the vault that displays all your captured ideas as proof cards with their timestamps and hashes.

Build the Vault

Your capture form is working. Ideas are flowing into Firestore with their SHA-256 fingerprints and server timestamps. But right now, the only way to see them is by digging through the Firebase Console.

In this step, you will build the vault. This is the screen where all captured ideas appear as "proof cards" showing the content, when it was captured, and its cryptographic fingerprint. The vault updates in real time, so new ideas appear the instant they are saved.

In this step, get ready to:
  • Subscribe to your ideas collection with real-time updates.
  • Build an IdeaCard component to display each proof card.
  • Render the vault below the capture form with a mobile-first layout.
Subscribe to the ideas collection in real time

Firestore's onSnapshot listener pushes updates to your app the moment data changes on the server. Instead of fetching once and hoping the data is fresh, your vault will always reflect the latest state.

  • In src/app/dashboard/page.js, find the imports at the top of the file.
  • Replace the entire import section and state declarations with the following code:
"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { collection, query, orderBy, onSnapshot } from "firebase/firestore";
import { signOut } from "firebase/auth";
import { db, auth } from "@/lib/firebase";
import CaptureForm from "@/components/CaptureForm";
import IdeaCard from "@/components/IdeaCard";

export default function DashboardPage() {
  const { user, loading } = useAuth();
  const router = useRouter();
  const [ideas, setIdeas] = useState([]);

What changed in the imports?

You added collection, query, orderBy, and onSnapshot from the Firestore SDK. These are the building blocks for a real-time subscription sorted by timestamp.

You also imported IdeaCard (you will create this component next) and added an ideas state array to hold the documents coming from Firestore.

  • Now add the real-time subscription. Find the existing useEffect that handles the redirect (the one with router.push("/login")). Directly below that entire useEffect block, add this new effect:
  useEffect(() => {
    if (!user) return;

    const q = query(
      collection(db, "users", user.uid, "ideas"),
      orderBy("createdAt", "desc")
    );

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const ideasData = snapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(),
      }));
      setIdeas(ideasData);
    });

    return () => unsubscribe();
  }, [user]);

What does this code do?

This effect runs whenever user changes. It builds a Firestore query targeting the path users/{userId}/ideas and sorts results by createdAt descending (newest first).

onSnapshot opens a persistent connection to Firestore. Every time a document is added, changed, or removed, the callback fires and updates the ideas state. The cleanup function (return () => unsubscribe()) closes the connection when the component unmounts or the user logs out.

  • Save src/app/dashboard/page.js.

Your editor may show a warning about the missing IdeaCard import. That is expected. You will create it next.

Build the IdeaCard component

Each idea in the vault needs a visual "proof card" that shows three things: the idea itself, when it was captured, and its SHA-256 fingerprint. This is what makes the vault more than a list of notes.

  • Create a new file at src/components/IdeaCard.js and add the following code:
"use client";

export default function IdeaCard({ idea }) {
  // Convert Firestore Timestamp to a readable date string
  const timestamp = idea.createdAt?.toDate
    ? idea.createdAt.toDate().toLocaleString()
    : "Pending...";

  return (
    <div className="border border-gray-200 rounded-lg p-4 bg-white">
      <p className="text-gray-900 mb-3">{idea.content}</p>
      <div className="border-t border-gray-100 pt-3 space-y-1">
        <p className="text-xs text-gray-500">
          <span className="font-medium">Captured:</span> {timestamp}
        </p>
        <p className="text-xs text-gray-500 font-mono break-all">
          <span className="font-medium font-sans">SHA-256:</span> {idea.hash}
        </p>
      </div>
    </div>
  );
}

What does this component do?

The component receives an idea object with content, createdAt, and hash fields. It converts the Firestore Timestamp into a human-readable string using toDate().toLocaleString().

The "Pending..." fallback handles the brief moment after saving when the server timestamp has not resolved yet. The SHA-256 hash is displayed in a monospace font with break-all so the 64-character string wraps cleanly on mobile screens.

  • Save src/components/IdeaCard.js.

Seeing a blank page or console error?

If you see an error mentioning a missing module, double-check the file path is exactly src/components/IdeaCard.js. The import in your dashboard expects this exact location.

Also confirm the file starts with "use client"; since it runs in the browser.

Help me debug my IdeaCard component not rendering.

Render the vault below the capture form

The subscription is running and the card component is ready. Now you need to display the list of ideas in the dashboard's JSX.

  • In src/app/dashboard/page.js, find the closing </section> tag after the <CaptureForm /> component.
  • Directly below that </section>, add the vault section by pasting this code:
        <section>
          <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">
            Your Vault ({ideas.length})
          </h2>
          <div className="space-y-3">
            {ideas.length === 0 ? (
              <p className="text-gray-400 text-center py-8">
                No ideas yet. Capture your first one above.
              </p>
            ) : (
              ideas.map((idea) => <IdeaCard key={idea.id} idea={idea} />)
            )}
          </div>
        </section>

What does this code do?

The vault section shows a header with the count of captured ideas (ideas.length). If the list is empty, it shows an encouraging placeholder message.

When ideas exist, it maps over the array and renders an IdeaCard for each one. The key={idea.id} prop helps React efficiently update only the cards that change.

  • Save src/app/dashboard/page.js.
  • Return to your browser at http://localhost:3000. You should see your previously captured ideas appear in the vault below the capture form, newest first.
  • Capture a new idea using the form. It should appear at the top of the vault instantly without a page refresh.

Vault is empty even though you captured ideas before?

Check your browser's developer console for errors. A common issue is a missing Firestore index. If you see an error message with a link to create an index, click it and wait for the index to build.

Also confirm you are signed in with the same Google account you used when capturing ideas earlier. Each user's ideas live under their own subcollection.

Help me figure out why my vault is not showing any ideas.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files.

ⓧ I'd like to double check the full code

Here is the complete src/app/dashboard/page.js:

"use client";

import { useAuth } from "@/context/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { collection, query, orderBy, onSnapshot } from "firebase/firestore";
import { signOut } from "firebase/auth";
import { db, auth } from "@/lib/firebase";
import CaptureForm from "@/components/CaptureForm";
import IdeaCard from "@/components/IdeaCard";

export default function DashboardPage() {
  const { user, loading } = useAuth();
  const router = useRouter();
  const [ideas, setIdeas] = useState([]);

  useEffect(() => {
    if (!loading && !user) {
      router.push("/login");
    }
  }, [user, loading, router]);

  useEffect(() => {
    if (!user) return;

    const q = query(
      collection(db, "users", user.uid, "ideas"),
      orderBy("createdAt", "desc")
    );

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const ideasData = snapshot.docs.map((doc) => ({
        id: doc.id,
        ...doc.data(),
      }));
      setIdeas(ideasData);
    });

    return () => unsubscribe();
  }, [user]);

  const handleSignOut = async () => {
    await signOut(auth);
  };

  if (loading || !user) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <p className="text-gray-500">Loading...</p>
      </div>
    );
  }

  return (
    <div className="min-h-screen pb-8">
      <header className="sticky top-0 bg-white border-b border-gray-200 px-4 py-3 flex items-center justify-between z-10">
        <h1 className="text-lg font-bold text-gray-900">Originly</h1>
        <button
          onClick={handleSignOut}
          className="text-sm text-gray-500 hover:text-gray-900"
        >
          Sign out
        </button>
      </header>

      <main className="max-w-lg mx-auto px-4 pt-6 space-y-6">
        <section>
          <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">
            Capture
          </h2>
          <CaptureForm />
        </section>

        <section>
          <h2 className="text-sm font-medium text-gray-500 uppercase tracking-wide mb-3">
            Your Vault ({ideas.length})
          </h2>
          <div className="space-y-3">
            {ideas.length === 0 ? (
              <p className="text-gray-400 text-center py-8">
                No ideas yet. Capture your first one above.
              </p>
            ) : (
              ideas.map((idea) => <IdeaCard key={idea.id} idea={idea} />)
            )}
          </div>
        </section>
      </main>
    </div>
  );
}

And here is the complete src/components/IdeaCard.js:

"use client";

export default function IdeaCard({ idea }) {
  const timestamp = idea.createdAt?.toDate
    ? idea.createdAt.toDate().toLocaleString()
    : "Pending...";

  return (
    <div className="border border-gray-200 rounded-lg p-4 bg-white">
      <p className="text-gray-900 mb-3">{idea.content}</p>
      <div className="border-t border-gray-100 pt-3 space-y-1">
        <p className="text-xs text-gray-500">
          <span className="font-medium">Captured:</span> {timestamp}
        </p>
        <p className="text-xs text-gray-500 font-mono break-all">
          <span className="font-medium font-sans">SHA-256:</span> {idea.hash}
        </p>
      </div>
    </div>
  );
}

Your vault is live. Ideas flow in, get fingerprinted, and appear as proof cards in real time. Next up, you will lock down the security rules and deploy the app so you can use it on your phone.

Secure and Deploy

Your app captures ideas, hashes them, and displays them in a real-time vault. Right now, though, two things are missing: security and accessibility.

Your Firestore database is still in test mode, meaning anyone with the URL could read or write data. You also need the app on a public URL so you can use it daily from your phone. In this step, you will lock down access with security rules and deploy to Vercel.

In this step, get ready to:
  • Replace test-mode Firestore rules with production security rules.
  • Push your project to GitHub and deploy it on Vercel.
  • Verify the live app works on your phone and blocks unauthorized access.
Lock down Firestore with security rules

Your Firestore database was created in test mode, which allows anyone to read and write all data. Production security rules restrict access so that only the authenticated owner of a document can read or write it.

  • Return to the Firebase Console in your browser.
  • In the left panel, go to Databases & Storage > Firestore.
  • Click the Rules tab at the top.
  • Select all the existing rules in the editor and delete them.
  • Paste in the following production rules:
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Each user can only access their own ideas subcollection
    match /users/{userId}/ideas/{ideaId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

What do these rules do?

The match /users/{userId}/ideas/{ideaId} path targets every document inside a user's ideas subcollection.

The condition request.auth != null && request.auth.uid == userId checks two things: the request comes from a signed-in user, and that user's UID matches the {userId} segment of the path. This means you can only read or write your own ideas.

  • Click Publish to deploy the rules.

You should see a confirmation message at the top of the editor indicating the rules are now live.

Seeing a syntax error in the editor?

Make sure rules_version = '2'; is the very first line with no extra spaces before it. The editor validates syntax in real time and highlights errors.

help me fix my Firestore security rules syntax error

Why not test mode in production?

Test mode grants full read and write access to every user, even unauthenticated ones. Anyone who discovers your project ID could delete all your data. Production rules ensure only you can see your ideas.

Push to GitHub and deploy on Vercel

Vercel connects directly to a GitHub repository and automatically deploys your app whenever you push code. You will create a repository, push your project, and then import it into Vercel.

  • Open GitHub in your browser and create a new repository named originly.
  • Leave it empty (no README, no .gitignore).
  • Initialize your local project as a Git repository and push it by running these commands in your terminal:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin https://github.com/[[GITHUB_USERNAME="your-github-username"]]/originly.git
git push -u origin main

What do these commands do?

  • git init creates a new Git repository in your project folder.
  • git add . stages all files for commit.
  • git remote add origin connects your local repo to the GitHub remote you just created.
  • git push -u origin main uploads your code to GitHub.
  • Refresh your GitHub repository page. You should see all your project files listed.

Push failed with authentication error?

GitHub requires a personal access token or SSH key for pushing. If you see an authentication error, generate a personal access token in GitHub Settings > Developer Settings > Personal Access Tokens and use it as your password when prompted.

help me authenticate with GitHub from the terminal

Now connect the repository to Vercel for deployment.

  • Go to vercel.com in your browser.
  • Click Sign Up and choose Continue with GitHub. This creates a free Hobby plan account.
  • Authorize Vercel to access your GitHub account when prompted.
  • Once signed in, click Add New... in the top-right corner and select Project.
  • Find and select your originly repository from the list.
  • On the configuration page, expand the Environment Variables section.
  • Add each of the following environment variables one at a time, copying the values from your .env.local file:
  • NEXT_PUBLIC_FIREBASE_API_KEY
  • NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
  • NEXT_PUBLIC_FIREBASE_PROJECT_ID
  • NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
  • NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
  • NEXT_PUBLIC_FIREBASE_APP_ID
  • Click Deploy.

Vercel will build your Next.js app and assign it a public URL ending in .vercel.app. The build takes about a minute.

Why environment variables on Vercel?

Your .env.local file only exists on your machine. Vercel needs its own copy of those values to build and serve your app. The NEXT_PUBLIC_ prefix tells Next.js to expose them to the browser at build time.

Build failing on Vercel?

The most common cause is a missing environment variable. Go to your Vercel project settings, check that all six NEXT_PUBLIC_FIREBASE_* variables are present, and redeploy. You must redeploy after adding variables for them to take effect.

help me debug my Vercel deployment failure for a Next.js Firebase app

Test the live app on your phone

The whole point of this app is daily mobile use. Time to confirm everything works outside your development machine.

  • Copy the .vercel.app URL that Vercel shows after deployment.
  • Open the URL on your phone's browser.
  • Sign in with your Google account.
  • Capture a new idea and confirm it appears in the vault with its SHA-256 hash and timestamp.
  • Open the same URL in an incognito/private browser window on your computer (without signing in).
  • Confirm that unauthenticated users only see the login page.

Test with another Google account

If you have a second Google account, sign in with it and confirm that its vault is completely empty. Your security rules prevent one user from seeing another user's ideas.

App loads but sign-in fails on the live URL?

Firebase Authentication restricts which domains can trigger sign-in. Return to the Firebase Console, go to Authentication > Settings > Authorized domains, and add your .vercel.app domain.

help me authorize my Vercel domain in Firebase Authentication settings

Secret mission

Add an Integrity Check

Your app captures ideas and proves when they were created. But what if someone claims they had the same idea first? Add a verification feature that lets you paste any text and instantly confirm whether it matches a previously captured idea's cryptographic fingerprint.

Clean Up Your Resources

Clean Up Your Resources

This project runs on Firebase's free Spark plan and Vercel's free Hobby plan, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Firebase project ("Originly") with Authentication and Firestore database.
  • Vercel deployment (live .vercel.app URL).
  • GitHub repository (connected to Vercel).
  • Local project files (originly folder on your Desktop).

Keep everything running

No action needed. Choose this if you plan to keep using Originly daily or want to continue experimenting with it.

  • Your Firebase Spark plan has no billing. It includes 1 GiB of Firestore storage, 50K reads per day, and 20K writes per day at no cost.
  • Your Vercel Hobby plan is free with no billing cycles.
  • Your GitHub repository stays private (or public) with no cost.
  • Your local originly folder remains on your Desktop for future edits.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your cloud resources are on free plans, so they can stay live without cost.

  • In your terminal, press Ctrl+C to stop the local development server if it is still running.
  • Your Firebase project and Vercel deployment will stay live at no cost. No further action needed for those.
  • When you return, open the originly folder and run npm run dev to resume local development.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

  • Delete your Firebase project:
  • Delete your Vercel project:
  • Delete your GitHub repository:
  • Delete your local project files:
rm -rf ~/Desktop/originly
  • On Windows, run this in PowerShell:
Remove-Item -Recurse -Force ~\Desktop\originly

Nice Work!

Nice Work!

Nice work! You just built and deployed a mobile-first idea capture app that generates cryptographic proof-of-existence every time you save a thought.

You've learned how to:

  • Build a mobile-first Next.js app with Firebase Google authentication to protect user data behind a secure sign-in flow.
  • Implement client-side SHA-256 hashing using the browser's Web Crypto API to generate tamper-evident fingerprints of captured ideas.
  • Deploy a live app on Vercel, secured with Firestore security rules so only the authenticated owner can read and write their ideas.
  • Secret Mission: Added an integrity verification feature that cryptographically proves an idea has not been modified since its original capture date.

Ready to quiz yourself?