A simple wishlist management application built with Go, HTMX, and SQLite. Features server-side rendering, minimal JavaScript, collapsible family sections, and a clean responsive UI with a fresh mint color scheme.
- 📝 Create and manage wishlist items for family members
- 👥 Add and organize family members with collapsible sections
- ✅ Mark items as reserved/available with pure CSS dropdown (no extra HTTP requests)
- 🔐 Password-protected editing mode
- 📧 Email notifications for additions, reservation changes, and deletions
- 🎨 Clean, responsive UI with fresh mint color scheme
- ⚡ HTMX for dynamic updates with minimal page reloads
- 🔒 Session-based authentication
- 📦 Uses templ for type-safe Go templates
- Go 1.26 or higher
- A C compiler toolchain (required because the
go-sqlite3driver uses CGO) - Node.js 20.19 or higher and npm (for CSS linting and formatting, optional - see Development Tools section)
golangci-lint(optional, for development - see Development Tools section)
- Clone the repository:
git clone https://github.com/macurovc/wishpage.git
cd wishpage- Install dependencies:
go mod download- Set up environment variables:
# Required for edit mode
export EDIT_PASSWORD="your-secure-password"
# Optional configuration
export DATABASE_PATH="/path/to/wishlist.db"
export PORT="3002"
# Optional: Email notifications (see Email Notifications section)
export EMAIL_USER="your-email@gmail.com"
export EMAIL_PASS="your-app-password"
export EMAIL_TO="recipient@example.com"- Run the application:
go run .- Open your browser to
http://localhost:3002
| Variable | Description | Default |
|---|---|---|
PORT |
Server port | 3002 |
DATABASE_PATH |
Path to SQLite database | ./wishlist.db (an existing legacy temp database is detected automatically) |
EDIT_PASSWORD |
Password for edit mode | (required for editing) |
EMAIL_HOST |
SMTP server hostname | smtp.gmail.com |
EMAIL_PORT |
SMTP server port | 587 |
EMAIL_USER |
SMTP username | (required for email) |
EMAIL_PASS |
SMTP password | (required for email) |
EMAIL_FROM |
Sender email address | Same as EMAIL_USER |
EMAIL_TO |
Recipient email address | (required for email) |
The application sends automatic email notifications for these wishlist changes:
- Items added, reserved, or deleted
- Family members added or deleted
Important: Email is completely optional. If not configured, the application works normally without sending notifications.
- Enable 2-Factor Authentication on your Google account
- Generate an App Password at https://myaccount.google.com/apppasswords
- Set environment variables:
export EMAIL_USER=your-email@gmail.com
export EMAIL_PASS=your-16-char-app-password
export EMAIL_TO=recipient@example.comOffice 365 / Outlook:
export EMAIL_HOST=smtp.office365.com
export EMAIL_PORT=587
export EMAIL_USER=your-email@outlook.com
export EMAIL_PASS=your-password
export EMAIL_TO=recipient@example.comCustom SMTP Server:
export EMAIL_HOST=smtp.example.com
export EMAIL_PORT=587 # or 465 for TLS
export EMAIL_USER=your-username
export EMAIL_PASS=your-password
export EMAIL_TO=recipient@example.com"Email service verification failed"
- Check SMTP hostname and port are correct
- Verify firewall allows outbound SMTP connections
- Corporate networks may block SMTP ports
"Authentication failed"
- Gmail: Use App Password, not your regular password
- Verify username and password are correct
- Check email provider security settings
Emails not received
- Check spam/junk folder
- Verify
EMAIL_TOaddress is correct - Check server logs for errors
- Some providers have rate limits
The project includes a Makefile with convenient commands for development:
# Validate everything (format, lint, test, tidy) - run this before committing!
make validate
# Show all available commands
make help
# Individual Go commands:
make fmt # Format all Go code with go fmt
make lint # Run golangci-lint with comprehensive checks
make test # Run all tests with race detection and coverage
make tidy # Clean up go.mod and verify dependencies
# CSS commands:
make fmt-css # Format CSS files with Prettier
make fmt-css-check # Check CSS formatting without modifying files
make lint-css # Run Stylelint on CSS files
make lint-css-fix # Run Stylelint and auto-fix CSS issues
# Utility commands:
make clean # Remove build artifacts, caches, and node_modules
make check-deps # Verify all required tools are installed
make npm-install # Install npm dependencies for CSS toolsPrerequisites for development tools:
Go Tools:
golangci-lintis required formake lintandmake validate- Install on macOS:
brew install golangci-lint - Install on Linux:
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin - See installation guide for other platforms
CSS Tools:
- Node.js 20.19 or higher and npm are required for CSS linting and formatting
- Install from nodejs.org or via package manager
- Run
npm installormake npm-installto install Stylelint and Prettier - The CI pipeline automatically checks CSS formatting and linting
Recommended workflow:
- Run
make validatebefore committing changes - This ensures code is formatted (Go and CSS), passes linting, and all tests pass
- The Go linter runs configured checks for code quality, security, and style
- The CSS linter enforces best practices and prevents common mistakes
CI/CD Pipeline:
- GitHub Actions automatically runs on all pushes and pull requests
- Parallel jobs check Go tests, Go linting, CSS formatting, CSS linting, and build
- All checks must pass before merging
- Coverage reports are uploaded to Codecov
# Run all tests
go test ./...
# Run tests with verbose output
go test -v ./...
# Run specific test
go test -v -run TestEmailService
# Run tests with coverage (or use 'make test')
go test -v -race -coverprofile=coverage.out ./...This project uses templ for type-safe HTML templates. After modifying .templ files, regenerate the Go code:
# Install or update to the templ version pinned by go.mod
go install "github.com/a-h/templ/cmd/templ@$(go list -m -f '{{.Version}}' github.com/a-h/templ)"
# Generate template code
templ generateNote: The generated *_templ.go files are checked into version control. Always regenerate them after editing .templ files and include both the .templ and *_templ.go files in your commits.
wishpage/
├── main.go # Application entry point and server setup
├── auth.go # Authentication and session management
├── database.go # Database initialization
├── handlers.go # Main page handlers
├── items.go # Item CRUD operations
├── family_members.go # Family member CRUD operations
├── email_service.go # Email notification service
├── helpers.go # Utility functions
├── models/
│ └── models.go # Data models
├── templates/
│ ├── layout.templ # Base layout template
│ ├── view.templ # Public view templates
│ ├── edit.templ # Edit mode templates
│ ├── login.templ # Login page template
│ └── error.templ # Error template
├── static/
│ ├── css/
│ │ └── styles.css # Application styles
│ └── js/
│ ├── htmx.min.js # Vendored HTMX runtime
│ └── htmx.LICENSE.txt
├── Makefile # Development commands (validate, test, lint, fmt)
├── .golangci.yml # Go linter configuration
├── .stylelintrc.json # CSS linter configuration (Stylelint)
├── .prettierrc.json # CSS formatter configuration (Prettier)
├── package.json # Node.js dependencies for CSS tooling
├── go.mod # Go module dependencies
└── *_test.go # Test files
Page Routes:
GET /- Landing page with collapsible sections for all family members and their itemsGET /login- Login pageGET /static/*- Static files (CSS, JS, images)
API Routes:
GET /api/items- Get all items (supports?family_member_id=Xfilter)PUT /api/items/{id}/reserve- Mark item as reserved, returns updated item card HTML for HTMXGET /api/family_members- Get all family members
Authentication:
POST /api/login- Login with password, returns session cookiePOST /api/logout- Logout and clear session
These authentication endpoints do not require an existing session.
Page Routes:
GET /edit- Edit mode page (full list of items and family members)
Item Management:
POST /api/items- Create new item (requires name, family_member_id; optional link, price)PUT /api/items/{id}- Update item detailsDELETE /api/items/{id}- Delete itemPUT /api/items/{id}/unreserve- Mark item as unreserved (admin only)
Family Member Management:
POST /api/family_members- Create family memberPUT /api/family_members/{id}- Update family member nameDELETE /api/family_members/{id}- Delete family member (cascades to items)
The application bundles HTMX 1.9.10 into the executable for dynamic updates without full page reloads:
- Item reservation uses pure CSS
<details>dropdown for confirmation (no HTTP request until confirmed) - Reservation confirmation updates just the single item card in-place
- Adding/deleting items updates the list dynamically
- Family member management happens without page navigation
- Collapsible family sections use native HTML
<details>elements - Smooth transitions and feedback
- Secure session tokens with automatic expiration (24 hours)
- Cookie-based authentication (HttpOnly, SameSite=Strict)
- Background cleanup of expired sessions (runs hourly)
- Concurrent access handling with thread-safe session store
- Constant-time password comparison to prevent timing attacks
- SQLite for simplicity and portability
- Foreign key constraints enabled on every connection
- Cascade deletes for family members (deleting a member removes all their items)
- Automatic schema creation on first run
- Items automatically ordered by price (items without price shown first)
All application data lives in one SQLite file. Back it up periodically with SQLite's online backup command or a snapshot of the persistent volume:
sqlite3 /path/to/wishlist.db ".backup '/path/to/wishlist-backup.db'"Avoid copying the live database file directly while the service may be writing to it.
When configured, the application sends HTML-formatted email notifications for additions, reservation changes, and deletions:
- Items: added, reserved/unreserved, deleted (includes name, family member, price, link)
- Family Members: added or deleted
The email service supports:
- Both TLS (port 465) and STARTTLS (port 587) connections
- Gmail, Office 365, and custom SMTP servers
- Graceful degradation (app continues if email fails)
- Automatic HTML escaping for security
- Password-protected edit mode with constant-time comparison
- Session-based authentication with automatic expiration (24 hours)
- HTTPS-only session cookies with Secure, HttpOnly, and SameSite=Strict attributes
- Input validation and sanitization
- SQL injection prevention through parameterized queries
- Email credentials stored in environment variables (never committed)
- HTML escaping in email notifications prevents injection attacks
Session cookies always use the Secure, HttpOnly, and SameSite=Strict
attributes. Login and edit mode therefore require an HTTPS connection between
the browser and the public application URL.
This works with Cloudflare Tunnel and other HTTPS reverse proxies even when the proxy connects to the container over HTTP: the browser sees HTTPS and applies the cookie security policy there.
Direct HTTP remains available for the public wishlist, but authenticated edit
mode requires HTTPS. For local development, modern browsers normally permit
Secure cookies on http://localhost; use an HTTPS proxy for other hostnames.
This application uses Go's embed directive to include all static files (CSS, JS, images) directly in the binary. This means you only need to deploy a single executable file - no separate static directory required!
-
Validate the code before building (recommended):
make validate
-
Build the Docker image:
docker build -t wishpage . -
Run the Docker container:
docker run -d \ -p 3002:3002 \ -e EDIT_PASSWORD=your_super_secret_password \ -e DATABASE_PATH=/data/wishlist.db \ -v $(pwd)/data:/data \ --name wishpage \ wishpage- The
-eflag sets environment variables - The
-vflag mounts a local directory (./data) into the container at/datafor database persistence - Without the volume mount, your data will be lost if the container is removed
- The
-
Access the application:
The public wishlist will be accessible at
http://localhost:3002. Expose production deployments through HTTPS, such as a Cloudflare Tunnel, to use login and edit mode.
- Validate the code before building:
make validateThis ensures your code is properly formatted, passes all linter checks, and all tests pass.
- Build the application:
go build -o wishpageThis creates a single, self-contained binary with all static files embedded.
- Copy the binary to your server:
scp wishpage your-server:/opt/wishpage/- Set environment variables on your server:
export EDIT_PASSWORD="your-secure-password"
export DATABASE_PATH="/var/lib/wishpage/wishlist.db"
# Optional: Email configuration
export EMAIL_USER="your-email@gmail.com"
export EMAIL_PASS="your-app-password"
export EMAIL_TO="recipient@example.com"- Run the binary:
./wishpageThat's it! No need to copy the static/ directory - it's already in the binary.
Additional recommendations:
- Consider using a process manager like systemd or supervisor (see example below)
- Put behind a reverse proxy (nginx/Caddy) for HTTPS
Create /etc/systemd/system/wishpage.service:
[Unit]
Description=Wishpage Application
After=network.target
[Service]
Type=simple
User=wishpage
WorkingDirectory=/opt/wishpage
ExecStart=/opt/wishpage/wishpage
Environment="EDIT_PASSWORD=your-password"
Environment="DATABASE_PATH=/var/lib/wishpage/wishlist.db"
Environment="PORT=3002"
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetThen:
sudo systemctl daemon-reload
sudo systemctl enable wishpage
sudo systemctl start wishpage- Default location: If
DATABASE_PATHis not set, the database is created as./wishlist.dbin the working directory - Legacy location: If no local database exists but the previous temp-directory database does, the application continues using it and logs a migration notice
- Ensure the database file path is writable
- Check file permissions (the application needs read/write access)
- Verify a C compiler toolchain is installed so the go-sqlite3 driver can build
- Verify all required variables are set:
EMAIL_USER,EMAIL_PASS,EMAIL_TO - Check server logs for detailed error messages
- Look for "Email service is configured correctly and ready to send messages." on startup
- Test with a simple action (add an item) and check spam folder
- Ensure firewall allows outbound SMTP connections
- Verify
EDIT_PASSWORDis set - Verify the browser is using the HTTPS application URL
- Clear browser cookies and try again
- Check server logs for session-related errors
Contributions are welcome! Please ensure:
- All validation checks pass:
make validate - Templates are regenerated:
templ generate(if you modified.templfiles) - CSS linting passes if you modify styles:
make lint-css
The make validate command will automatically:
- Format your Go code with
go fmt - Format your CSS with Prettier
- Run comprehensive linting checks (Go and CSS)
- Run all tests with race detection
- Verify dependencies are clean
See the LICENSE file in the root of the repository.