StoryBook Journal - Next.js, TypeScript, PostgreSQL, Tailwind CSS FullStack Project (Online/Offline Drafting & Sync Capability with IndexedDB & AI Writing Assistance Smart Diary)
A premium, immersive digital diary built with Next.js. It feels like opening a real leather notebook: 3D cover animations, CSS page-flip transitions, ruled paper, moods, weather, tags, autosave, offline drafts, and optional AI writing assistance — all backed by PostgreSQL and secure authentication.
- Live Demo: https://storybook-journal.vercel.app
- What Is This Project?
- Key Features
- Technology Stack
- How It Works (Architecture)
- Project Structure
- Routes & Pages
- API Endpoints
- Database Schema
- Environment Variables
- Getting Started
- Available Scripts
- Learning Walkthrough
- Reusable Components & Patterns
- Code Examples
- Keywords & SEO
- Related Documentation
- License
StoryBook Journal is a full-stack journaling SaaS-style web application — not a simple notes CRUD demo. The product goal is emotional and tactile:
- Open a leather book cover with a 3D hinge animation
- Browse multiple journals on a bookshelf dashboard
- Flip pages between daily entries like a real diary
- Write with rich content, mood/weather pickers, and tags
- Autosave while typing; survive refreshes with offline drafts
- Sync queued changes when the network returns
- Get AI writing suggestions through a secure server proxy (optional)
This repository is designed for learning: Server Components for data, Client Components for animation, REST Route Handlers for APIs, Prisma for persistence, TanStack Query for client cache, and IndexedDB for offline resilience.
| Feature | Description |
|---|---|
| 3D book cover | Leather texture, gold shine sweep, hover tilt, animated cover open on landing |
| Page-flip navigation | CSS preserve-3d flip overlay; route changes fire after animation completes |
| Book spread UI | Left page = previous entry + entry list; right page = read/write mode |
| Multiple journals | Create books with custom cover color + emoji on the shelf |
| Rich entry metadata | Mood, weather, location, tags, word count, reading time |
| Autosave | Debounced PATCH while editing (2 s delay) |
| Offline-first drafts | IndexedDB entry drafts + sync queue for PATCH/POST when offline |
| Optimistic UI | TanStack Query cache updates instantly; server sync reconciles later |
| Authentication | Email/password (bcrypt) + optional Google OAuth |
| Demo login | Pre-filled test account dropdown on /login for quick exploration |
| AI writing assist | Anthropic Claude via /api/ai/assist (sync + SSE stream); key never exposed to browser |
| Security headers | X-Frame-Options, CSP-friendly patterns, dashboard noindex |
| SafeImage | Remote avatar loading with Robohash fallback |
| Responsive book sizing | CSS clamp() tokens (--page-w, --page-h) scale to viewport |
Each library plays a specific role. Understanding why it is here helps you reuse patterns in other projects.
| Library | Version | Role |
|---|---|---|
| Next.js | 16 | App Router, Server Components, Route Handlers, metadata API |
| React | 19 | UI rendering, hooks, client/server component split |
| TypeScript | 5.7 | Strict typing across API, DB, and UI |
Next.js App Router means pages under src/app/ are routes. Files named page.tsx render URLs; layout.tsx wraps nested routes; route.ts files inside api/ become REST endpoints.
| Library | Role |
|---|---|
| Prisma | ORM — type-safe PostgreSQL queries, migrations, schema |
| PostgreSQL | Relational database for users, books, entries |
| Zod | Runtime validation shared between API routes and forms |
| bcryptjs | Password hashing before storing in User.passwordHash |
| Library | Role |
|---|---|
| NextAuth v5 | JWT sessions, Credentials + Google providers, auth() helper |
Sessions use HttpOnly cookies (via NextAuth). The user id from Prisma is copied into the JWT so API routes can scope queries with session.user.id.
| Library | Role |
|---|---|
| TanStack Query | Server-state cache, prefetch, invalidation after CRUD |
| React Hook Form | Login/register form state |
| Sonner | Toast notifications (save success, offline queue, errors) |
| Framer Motion | Available for motion (book uses mostly custom CSS animations) |
| TipTap | Rich text editor extensions (starter kit in dependencies) |
| date-fns | Format entry dates (MMMM d, yyyy, weekday names) |
| lucide-react | Icon set for nav and UI chrome |
| Library | Role |
|---|---|
| Tailwind CSS | Utility classes for forms, nav, modals |
Custom CSS (globals.css) |
Book aesthetic: leather, paper, flip keyframes, design tokens |
┌─────────────┐ ┌──────────────────┐ ┌─────────────┐ ┌──────────────┐
│ Browser │────▶│ Next.js (App) │────▶│ Prisma │────▶│ PostgreSQL │
│ React UI │◀────│ Server + API │◀────│ Client │◀────│ │
└─────────────┘ └──────────────────┘ └─────────────┘ └──────────────┘
│ │
│ IndexedDB │ NextAuth JWT
▼ ▼
Offline drafts Session cookies
Sync queue- User opens
/journal/[bookId]. src/proxy.ts(Next.js 16 edge boundary) checks session; redirects to/loginif missing.journal/[bookId]/page.tsx(Server Component) callsauth()+prisma.journalBook.findFirstwith ownership check.- Data is passed as
initialBooktoBookSpread(Client Component). - User flips pages →
usePageFlipanimates → index changes locally. - User edits →
useAutoSavedebounces →PATCH /api/entries/[entryId]. - On success →
queryClient.invalidateQueries({ queryKey: queryKeys.journalSubtree() })refreshes shelf counts everywhere.
| Layer | Pattern |
|---|---|
| Server Components | Data fetch in page.tsx — no client bundle for Prisma |
| Client Components | "use client" for flip, write mode, forms, offline hooks |
| API Route Handlers | src/app/api/**/route.ts — JSON REST, Zod validation, auth() guard |
When the browser is offline (or fetch fails):
useOfflineEntryDraftsaves draft text to IndexedDB.offline-journal-actionsenqueues PATCH/POST to the sync queue with temp ids (offline-entry-*,offline-book-*).useOfflineSyncQueuedrains the queue onwindow.online.useOfflineIdRemapswaps temp ids to server cuids so the reader stays on the correct page.DashboardNavshows an{n} offlinebadge viaOfflineSyncContext.
storybook-journal/
├── prisma/
│ ├── schema.prisma # User, JournalBook, JournalEntry models
│ ├── seed.ts # Optional seed script
│ └── migrations/ # SQL migration history
├── public/ # Static assets (SVG icons, book stack art)
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── layout.tsx # Root layout + SEO metadata
│ │ ├── page.tsx # Landing (3D cover) → redirect if logged in
│ │ ├── providers.tsx # SessionProvider, QueryClient, OfflineSync
│ │ ├── robots.ts # SEO robots rules
│ │ ├── (auth)/ # Login & register (book-spread auth UI)
│ │ ├── (dashboard)/ # Protected shelf + journal reader
│ │ └── api/ # REST Route Handlers
│ ├── components/
│ │ ├── auth/ # AuthBookShell, Google OAuth, OAuthReturnSync
│ │ ├── journal/ # BookSpread, BookShelf, PageFlip, BookCover
│ │ ├── forms/ # LoginForm, RegisterForm
│ │ ├── layout/ # DashboardNav
│ │ ├── feedback/ # ConfirmDialog
│ │ └── ui/ # safe-image, dropdown-menu (shadcn-style)
│ ├── context/
│ │ └── OfflineSyncContext.tsx
│ ├── hooks/ # usePageFlip, useAutoSave, offline hooks
│ ├── lib/ # auth, db, validations, journal-api, offline, AI
│ ├── constants/ # MOODS, WEATHERS, cover colors, offline events
│ ├── types/ # Shared TS interfaces
│ └── proxy.ts # Auth redirect middleware (Next.js 16+)
├── docs/ # Extended guides (auth UI, walkthrough, guardrails)
├── .env.example # Template for all environment variables
├── docker-compose.yml # Optional local Postgres only (not the app)
├── next.config.ts # Security headers, image remotePatterns
├── vercel.json # Vercel header overrides
└── package.json| Route | Type | Description |
|---|---|---|
/ |
Server | Landing cover animation; redirects to /dashboard if authenticated |
/login |
Server | Login form inside book spread; optional Google OAuth |
/register |
Server | Registration; creates user + welcome book + entry |
/dashboard |
Server | Bookshelf — lists all journals for the user |
/journal/[bookId] |
Server | Open book reader/writer (uses cuid bookId, not slug) |
/robots.txt |
Metadata | Disallows /api, /dashboard, /journal for crawlers |
Route groups (auth) and (dashboard) do not affect the URL — they organize layouts only.
Auth and dashboard pages use export const dynamic = "force-dynamic" so session data is always fresh (no stale cached HTML for private routes).
All JSON responses follow a common envelope:
{
"success": true,
"message": "Optional human message",
"data": {}
}| Method | Path | Auth | Description |
|---|---|---|---|
| GET, POST | /api/auth/[...nextauth] |
— | NextAuth session, sign-in, sign-out, OAuth callbacks |
| POST | /api/auth/register |
— | Register user; seeds default book + welcome entry |
| GET | /api/books |
✅ | List user's journals with entry counts |
| POST | /api/books |
✅ | Create journal + starter entry (transaction) |
| GET | /api/books/[bookId] |
✅ | Book detail + entries (ownership scoped) |
| PATCH | /api/books/[bookId] |
✅ | Update title, cover, description; slug sync on title change |
| DELETE | /api/books/[bookId] |
✅ | Delete book and cascade entries |
| POST | /api/entries |
✅ | Create entry in owned book |
| PATCH | /api/entries/[entryId] |
✅ | Autosave / manual save; recalculates wordCount, excerpt, slug |
| DELETE | /api/entries/[entryId] |
✅ | Delete entry (ownership scoped) |
| POST | /api/ai/assist |
✅ | Sync AI continuation (rate limited) |
| POST | /api/ai/assist/stream |
✅ | SSE streaming AI continuation |
| GET | /api/health |
— | Health check for monitoring |
Authorization pattern (every protected route):
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 },
);
}
// Prisma queries always include userId in where clauseThree main models in prisma/schema.prisma:
Stores account credentials and profile. passwordHash is null for OAuth-only users. Relations: books[], entries[].
A journal on the shelf. Unique constraint: @@unique([userId, slug]). Fields include coverColor, coverEmoji, theme, visibility.
A single diary page. tags is stored as a JSON string (e.g. "[\"morning\",\"coffee\"]") — parsed in app code with parseTags(). Unique: @@unique([bookId, slug]).
Cascade deletes: Deleting a user removes their books and entries.
Copy the template and fill in values:
cp .env.example .env| Variable | Description | How to obtain |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string for Prisma queries | Neon, Supabase, Railway, local Docker, or any Postgres host |
DIRECT_URL |
Direct Postgres URL for migrations/db push |
Same as DATABASE_URL unless you use a connection pooler (then use direct host for migrations) |
AUTH_SECRET |
NextAuth JWT signing secret | Run: openssl rand -base64 32 |
NEXTAUTH_URL |
Public app URL | http://localhost:3000 locally; https://your-domain.vercel.app in production |
Example .env for local development:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/storybook"
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/storybook"
AUTH_SECRET="your-generated-secret-here"
NEXTAUTH_URL="http://localhost:3000"Note: There is no
.envcommitted to the repo (by design). You must create one from.env.example. WithoutDATABASE_URLandAUTH_SECRET, the app cannot authenticate or persist data.
| Variable | Feature | Behavior when missing |
|---|---|---|
GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET |
Google OAuth login | Google button hidden; email/password still works |
GOOGLE_ID + GOOGLE_SECRET |
Legacy aliases | Same as above — supported for backward compatibility |
ANTHROPIC_API_KEY |
AI writing assist | Returns a poetic placeholder sentence; UI still works |
SHOW_DEMO_LOGIN |
Demo account dropdown on login | Defaults to on; set "false" to hide in production |
| Environment | Location |
|---|---|
| Local | .env or .env.local in project root |
| Vercel | Project → Settings → Environment Variables |
| CI | GitHub Actions secrets or your pipeline |
- Node.js 20+ (LTS recommended)
- npm 10+
- PostgreSQL 14+ (hosted or local)
# 1. Clone and enter the project
git clone https://github.com/your-username/storybook-journal.git
cd storybook-journal
# 2. Install dependencies
npm install
# 3. Configure environment
cp .env.example .env
# Edit .env — set DATABASE_URL, DIRECT_URL, AUTH_SECRET, NEXTAUTH_URL
# 4. Prepare database
npx prisma generate
npm run db:push
npm run db:seed # optional demo data
# 5. Start dev server
npm run devWhen the demo login picker is enabled on /login:
| Field | Value |
|---|---|
test@user.com |
|
| Password | 12345678 |
The test account is created automatically when the first real user registers (idempotent seed in the register API route).
If you do not have a hosted database:
docker compose up -d dbThen uncomment the Docker lines in .env.example:
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/storybook"
DIRECT_URL="postgresql://postgres:postgres@localhost:5432/storybook"Run npm run db:push again. Docker runs Postgres only — the Next.js app runs with npm run dev, not in Docker.
- Push repo to GitHub.
- Import project in Vercel.
- Add all required env vars in the dashboard.
- Deploy — Vercel runs
next buildautomatically. - For Google OAuth, add your Vercel URL to Google Cloud Console authorized origins and redirect URIs.
| Script | Command | Purpose |
|---|---|---|
| Dev server | npm run dev |
Hot reload at localhost:3000 |
| Production build | npm run build |
Compile for deployment |
| Start production | npm run start |
Run built app |
| Lint | npm run lint |
ESLint check |
| Lint fix | npm run lint:fix |
Auto-fix lint issues |
| Typecheck | npm run typecheck |
tsc --noEmit |
| Prisma generate | npm run db:generate |
Regenerate Prisma client after schema changes |
| Push schema | npm run db:push |
Sync schema to DB (dev-friendly) |
| Migrate | npm run db:migrate |
Create/apply migrations |
| Prisma Studio | npm run db:studio |
Visual DB browser |
| Seed | npm run db:seed |
Run prisma/seed.ts |
| Full setup | npm run setup |
install + generate + db push |
Follow this path if you are new to the codebase.
- Start at
src/app/page.tsx— session check +LandingCover. - Read
src/components/journal/BookCover.tsx— 3D hinge CSS animation. - Explore
src/app/(auth)/login/page.tsxandAuthBookShell.tsx— page flip between login/register.
src/app/(dashboard)/dashboard/page.tsx— Server Component fetches books.src/components/journal/BookShelf.tsx— client shelf, create book modal, prefetch on hover.
src/app/(dashboard)/journal/[bookId]/page.tsx— loads book + entries server-side.src/components/journal/BookSpread.tsx— the orchestrator: flip, autosave, offline, AI.LeftPage.tsx/RightPage.tsx— read vs write UI.src/hooks/usePageFlip.ts— animation timing and re-entrancy guard.
- Pick any route in
src/app/api/entries/[entryId]/route.ts. - Compare with
src/lib/validations.ts— same Zod shapes the API expects. - See
src/lib/journal-slug.ts— slug uniqueness when titles change.
src/lib/query-keys.ts— central cache keys.- After any save, search for
journalSubtree()invalidation — one call refreshes shelf + open book.
src/lib/offline/idb.ts— IndexedDB wrapper.src/hooks/useOfflineSyncQueue.ts— FIFO drain on reconnect.src/context/OfflineSyncContext.tsx— global pending count for nav badge.
These pieces are designed to be copied into other Next.js projects.
Wraps next/image with a fallback chain: primary URL → Robohash → native <img>.
Use when: loading user avatars from Google/GitHub where URLs may 404.
import { SafeImage } from "@/components/ui/safe-image";
<SafeImage
src={session.user.image}
alt="Avatar"
width={32}
height={32}
fallbackSrc={`https://robohash.org/${email}?size=64x64`}
/>;Manages flip animation state; calls your callback only after the animation ends (prevents blank flashes during navigation).
Use when: building book/carousel UIs where content must swap post-animation.
const { isFlipping, flipDir, triggerFlip } = usePageFlip();
triggerFlip("fwd", () => {
router.push("/next-page"); // safe — runs after 650ms flip
});Single invalidation root for related queries.
Use when: multiple useQuery hooks share a domain and should refetch together after mutations.
await queryClient.invalidateQueries({ queryKey: queryKeys.journalSubtree() });Accessible async confirmation modal with loading state.
Use when: destructive actions (delete entry, delete book, sign out).
Copy src/lib/offline/* + useOfflineSyncQueue if you need queue-and-drain semantics for any REST API — not journal-specific at the storage layer.
Next.js 16+ replacement for middleware — wrap NextAuth and redirect unauthenticated users before pages render.
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"you@example.com","password":"securepass123","displayName":"You"}'const res = await fetch("/api/books", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Travel Log",
coverColor: "#1a3a5c",
coverEmoji: "✈️",
description: "Road trips and memories",
}),
});
const { data } = await res.json(); // { id: "cuid...", ... }await fetch(`/api/entries/${entryId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: "Morning thoughts",
content: "<p>Today I learned...</p>",
mood: "☕",
weather: "🌤️",
tags: ["learning", "morning"],
}),
});// src/app/(dashboard)/dashboard/page.tsx (simplified)
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/db";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const books = await prisma.journalBook.findMany({
where: { userId: session.user.id },
include: { _count: { select: { entries: true } } },
orderBy: { updatedAt: "desc" },
});
return <BookShelf initialBooks={books} />;
}Primary keywords (also defined in src/lib/site-metadata.ts):
StoryBook Journal, digital journal, online diary, journaling app, page flip animation, immersive writing, AI writing assistant, personal journal, daily journal, Next.js journal, React diary app, offline journal, Prisma PostgreSQL, NextAuth, TanStack Query
SEO notes:
- Public landing
/is indexable with full OpenGraph/Twitter metadata. - Dashboard and journal routes set
robots: { index: false }— private user content stays out of search indexes. src/app/robots.tsdisallows/api,/dashboard,/journalfor well-behaved crawlers.
| Document | Topic |
|---|---|
docs/PROJECT_WALKTHROUGH.md |
Deep architecture map and audit notes |
docs/AUTH_UI_IMPLEMENTATION_GUIDE.md |
OAuth flicker prevention, session patterns |
docs/VERCEL_PRODUCTION_GUARDRAILS.md |
Security headers, bot protection |
docs/SAFE_IMAGE_REUSABLE_COMPONENT.md |
SafeImage implementation details |
docs/DROPDOWN_TEST_CREDENTIALS_DOCS.md |
Demo account and NextAuth reference |
.env.example |
All environment variable templates with comments |
This project is licensed under the MIT License. Feel free to use, modify, and distribute the code as per the terms of the license.
This is an open-source project - feel free to use, enhance, and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://www.arnobmahmud.com.
Enjoy building and learning! 🚀
Thank you! 😊