A minimalist personal journaling web application with a dark, calm, intentional design. Built for reflections and quiet time entries with optional audio attachments.
Live: https://samjourn.al
Repository: https://github.com/cedrugs/samjourn.al
- Features
- Tech Stack
- Architecture
- Prerequisites
- Local Development
- Production Deployment
- Environment Variables
- Database Schema
- API Reference
- Authentication
- Project Structure
- License
- Two content categories: Journal and Quiet Time
- Rich text editor with formatting toolbar (Tiptap)
- Audio file uploads with custom waveform player
- Date-based URLs (
/journal/2025-01-01,/quiet-time/2025-01-01) - Draft and published post states
- Search, filter, and sort functionality
- Google OAuth authentication with allowlist-based access control
- Admin-only user seeding via environment variables
- Dynamic sitemap generation for SEO
- Open Graph and Twitter card meta tags
- Fully responsive dark theme with muted green accents
- Single Docker image deployment with Caddy reverse proxy
| Layer | Technology |
|---|---|
| Runtime | Bun |
| Backend | Fastify |
| Frontend | React 19, Vite |
| Database | PostgreSQL 17 |
| ORM | Drizzle ORM |
| Authentication | Better Auth |
| Styling | Tailwind CSS v4 |
| Rich Text | Tiptap |
| Object Storage | S3-compatible (for audio files) |
| Reverse Proxy | Caddy |
| Containerization | Docker |
+------------------+
| Caddy |
| (Port 80/443) |
+--------+---------+
|
+------------------+------------------+
| |
v v
+-------------------+ +-------------------+
| Static Files | | /api/* |
| (Frontend SPA) | | /sitemap.xml |
| /frontend/dist | | |
+-------------------+ +--------+----------+
|
v
+-------------------+
| Fastify Backend |
| (Port 3000) |
+--------+----------+
|
+-------------------+-------------------+
| |
v v
+-------------------+ +-------------------+
| PostgreSQL | | S3 Storage |
| (Port 5432) | | (Audio Files) |
+-------------------+ +-------------------+
- Bun v1.x
- Docker and Docker Compose
- PostgreSQL 17 (or use Docker Compose)
- S3-compatible storage (AWS S3, Cloudflare R2, MinIO)
- Google Cloud Console project with OAuth 2.0 credentials
git clone https://github.com/cedrugs/samjourn.al.git
cd samjourn.aldocker compose up db -dcp backend/.env.example backend/.envEdit backend/.env with your credentials:
DATABASE_URL=postgresql://samjournal:samjournal@localhost:5432/samjournal
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
AUTH_SECRET=generate-a-random-32-char-string
S3_ENDPOINT=https://your-s3-endpoint
S3_BUCKET=your-bucket-name
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_REGION=auto
FRONTEND_URL=http://localhost:5173cd backend && bun install
cd ../frontend && bun installcd backend && bun run drizzle-kit pushADMIN_EMAIL=your@email.com ADMIN_NAME="Your Name" bun run src/seed.ts- Go to Google Cloud Console
- Create OAuth 2.0 Client ID (Web application)
- Add authorized redirect URI:
http://localhost:5173/auth/callback - Copy Client ID and Client Secret to your
.envfile
Terminal 1 (Backend):
cd backend && bun run src/index.tsTerminal 2 (Frontend):
cd frontend && bun run devAccess the application at http://localhost:5173
- Create a
.envfile in the project root:
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
AUTH_SECRET=your-production-secret-min-32-chars
S3_ENDPOINT=https://your-s3-endpoint
S3_BUCKET=your-bucket-name
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_REGION=auto
FRONTEND_URL=https://yourdomain.com
ADMIN_EMAIL=admin@example.com
ADMIN_NAME=Admin-
Update Google OAuth redirect URI to:
https://yourdomain.com/auth/callback -
Build and run:
docker compose up --build -dThe application will be available on port 80.
# Build the image
docker build -t samjournal .
# Run with environment variables
docker run -p 80:80 --env-file .env samjournalOn container start, the following happens automatically:
- Database migrations run via
drizzle-kit push - Admin user is seeded if
ADMIN_EMAILis set - Fastify backend starts on port 3000
- Caddy starts and proxies requests on port 80
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string |
GOOGLE_CLIENT_ID |
Yes | Google OAuth client ID |
GOOGLE_CLIENT_SECRET |
Yes | Google OAuth client secret |
AUTH_SECRET |
Yes | Secret for signing auth tokens (min 32 chars) |
S3_ENDPOINT |
Yes | S3-compatible storage endpoint URL |
S3_BUCKET |
Yes | S3 bucket name for audio files |
S3_ACCESS_KEY |
Yes | S3 access key |
S3_SECRET_KEY |
Yes | S3 secret key |
S3_REGION |
No | S3 region (default: auto) |
FRONTEND_URL |
Yes | Public URL of the frontend |
ADMIN_EMAIL |
No | Email for seeded admin user |
ADMIN_NAME |
No | Name for seeded admin user (default: Admin) |
| Column | Type | Description |
|---|---|---|
| id | TEXT | Primary key |
| TEXT | Unique email address | |
| name | TEXT | Display name |
| image | TEXT | Profile image URL |
| emailVerified | BOOLEAN | Email verification status |
| createdAt | TIMESTAMP | Creation timestamp |
| updatedAt | TIMESTAMP | Last update timestamp |
| Column | Type | Description |
|---|---|---|
| id | TEXT | Primary key (UUID) |
| category | ENUM | journal or quiet-time |
| date | DATE | Date string (YYYY-MM-DD) |
| title | TEXT | Optional post title |
| content | TEXT | JSON content from Tiptap rich text editor |
| audioUrl | TEXT | URL to uploaded audio file |
| status | ENUM | draft or published |
| publishedAt | TIMESTAMP | Publication timestamp |
| createdAt | TIMESTAMP | Creation timestamp |
| updatedAt | TIMESTAMP | Last update timestamp |
Unique constraint on (category, date) ensures one post per category per day.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/posts |
List all posts (supports ?status=published) |
| GET | /api/posts/:category/:date |
Get post by category and date |
| GET | /sitemap.xml |
Dynamic sitemap for SEO |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/posts/:id |
Get post by ID |
| POST | /api/posts |
Create new post |
| PUT | /api/posts/:id |
Update post |
| DELETE | /api/posts/:id |
Delete post |
| POST | /api/posts/:id/audio |
Upload audio file |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/auth/get-session |
Get current session |
| POST | /api/auth/sign-in/social |
Initiate Google OAuth |
| POST | /api/auth/sign-out |
Sign out |
POST /api/posts
Content-Type: application/json
{
"category": "journal",
"date": "2025-01-01",
"title": "New Year Reflections",
"content": "{\"type\":\"doc\",\"content\":[{\"type\":\"paragraph\",\"content\":[{\"type\":\"text\",\"text\":\"Today I...\"}]}]}",
"status": "published"
}POST /api/posts/:id/audio
Content-Type: multipart/form-data
file: [audio file]Supported formats: mp3, wav, ogg, m4a, aac, flac, webm
Maximum file size: 50MB
Authentication uses Better Auth with Google OAuth.
Only users that exist in the database can sign in. New users are blocked at the OAuth callback. To grant access:
- Seed a user via
ADMIN_EMAILenvironment variable on startup - Or manually insert a user into the
userstable
- Go to Google Cloud Console
- Create a new project or select existing
- Navigate to APIs and Services > Credentials
- Create OAuth 2.0 Client ID (Web application)
- Add authorized redirect URIs:
- Development:
http://localhost:5173/auth/callback - Production:
https://yourdomain.com/auth/callback
- Development:
- Copy Client ID and Client Secret to environment variables
- Sessions are stored in PostgreSQL via Better Auth
- Cookies are httpOnly and secure in production
- Automatic session refresh on API calls
- Sessions expire based on Better Auth defaults
samjourn.al/
├── backend/
│ ├── src/
│ │ ├── db/
│ │ │ ├── index.ts # Database connection
│ │ │ └── schema.ts # Drizzle schema definitions
│ │ ├── lib/
│ │ │ ├── auth.ts # Better Auth configuration
│ │ │ └── s3.ts # S3 upload utility
│ │ ├── routes/
│ │ │ └── posts.ts # Posts API routes
│ │ ├── index.ts # Fastify server entry point
│ │ └── seed.ts # Admin user seeding script
│ ├── drizzle.config.ts # Drizzle ORM configuration
│ ├── package.json
│ └── tsconfig.json
├── frontend/
│ ├── public/
│ │ ├── favicon.svg # Cross favicon
│ │ ├── og-image.svg # Open Graph image
│ │ └── robots.txt # Robots configuration
│ ├── src/
│ │ ├── components/
│ │ │ ├── ui/ # shadcn/ui components
│ │ │ ├── AdminLayout.tsx # Admin layout wrapper
│ │ │ ├── AudioPlayer.tsx # Custom waveform audio player
│ │ │ ├── DatePicker.tsx # Custom date picker
│ │ │ ├── Editor.tsx # Tiptap rich text editor
│ │ │ ├── Layout.tsx # Public layout wrapper
│ │ │ └── PostContent.tsx # Post content renderer
│ │ ├── context/
│ │ │ └── AuthContext.tsx # Authentication context
│ │ ├── lib/
│ │ │ ├── api.ts # API client functions
│ │ │ └── auth-client.ts # Better Auth client
│ │ ├── pages/
│ │ │ ├── admin/
│ │ │ │ ├── AdminEditor.tsx
│ │ │ │ └── AdminPosts.tsx
│ │ │ ├── AuthCallback.tsx
│ │ │ ├── AuthError.tsx
│ │ │ ├── Home.tsx
│ │ │ ├── NotFound.tsx
│ │ │ └── PostPage.tsx
│ │ ├── styles/
│ │ │ └── global.css # Tailwind and theme configuration
│ │ ├── types/
│ │ │ └── index.ts # TypeScript type definitions
│ │ ├── App.tsx # React router configuration
│ │ ├── main.tsx # Application entry point
│ │ └── vite-env.d.ts # Vite environment types
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts
├── Caddyfile # Caddy reverse proxy configuration
├── docker-compose.yml # Docker Compose configuration
├── Dockerfile # Multi-stage Docker build
├── start.sh # Container startup script
└── README.md
OAuth Redirect Error
- Ensure
FRONTEND_URLmatches your domain - Verify Google OAuth redirect URI matches
{FRONTEND_URL}/auth/callback - Check that the user exists in the database before attempting login
Database Connection Issues
- Verify PostgreSQL is running and accessible
- Check
DATABASE_URLformat:postgresql://user:password@host:port/database - Run migrations:
bun run drizzle-kit push
S3 Upload Failures
- Verify S3 credentials and bucket permissions
- Check S3_ENDPOINT format (include https://)
- Ensure bucket exists and is accessible
Docker Port Issues
- Docker exposes port 80, not 3000
- Use
http://localhost(nothttp://localhost:3000) for Docker - Check Caddy configuration in
Caddyfile
| Environment | Frontend URL | Backend Port | Access URL |
|---|---|---|---|
| Development | http://localhost:5173 |
3000 | http://localhost:5173 |
| Docker | http://localhost |
3000 (internal) | http://localhost |
| Production | https://yourdomain.com |
3000 (internal) | https://yourdomain.com |
- Fork the repository
- Create a feature branch:
git checkout -b feature-name - Make your changes and test thoroughly
- Commit with clear messages:
git commit -m "Add feature description" - Push to your fork:
git push origin feature-name - Create a Pull Request
This project is licensed under the MIT License. See the LICENSE file for details.
Author: @cedrugs
Repository: https://github.com/cedrugs/samjourn.al