https://github.com/disane87/tap-and-tell
Tap & Tell is a modern, NFC-powered digital guestbook that transforms how guests leave their mark at events. Guests tap their phone on an NFC tag (or scan a QR code), and a beautiful multi-step wizard guides them through leaving their name, photo, and a personal message. No app install required! π±β¨
https://github.com/disane87/tap-and-tell
Last synced: 4 months ago
JSON representation
Tap & Tell is a modern, NFC-powered digital guestbook that transforms how guests leave their mark at events. Guests tap their phone on an NFC tag (or scan a QR code), and a beautiful multi-step wizard guides them through leaving their name, photo, and a personal message. No app install required! π±β¨
- Host: GitHub
- URL: https://github.com/disane87/tap-and-tell
- Owner: Disane87
- Created: 2026-01-29T10:13:28.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-02-07T21:23:12.000Z (5 months ago)
- Last Synced: 2026-02-07T23:54:30.895Z (5 months ago)
- Language: TypeScript
- Homepage:
- Size: 5.15 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Funding: .github/FUNDING.yml
Awesome Lists containing this project
README
[](https://nuxt.com/)
[](https://vuejs.org/)
[](https://www.typescriptlang.org/)
[](https://tailwindcss.com/)
[](https://pnpm.io/)
[](LICENSE)
[](https://github.com/Disane87/tap-and-tell/issues)
# π― Tap & Tell β NFC-Enabled Digital Guestbook
Hey there! π **Tap & Tell** is a modern, NFC-powered digital guestbook that transforms how guests leave their mark at events. Guests tap their phone on an NFC tag (or scan a QR code), and a beautiful multi-step wizard guides them through leaving their name, photo, and a personal message. No app install required! π±β¨
> Perfect for weddings π, birthday parties π, corporate events π’, or any gathering where you want to capture memories digitally!
> [!NOTE]
> **π€ AI-Aided Development (AIAD)**
>
> This project openly uses AI-assisted development (e.g. Claude Code) to accelerate workflows, improve code quality, and gain more development momentum. All AI-generated code is reviewed and approved by humans β this is not a vibe-coding project, but a deliberate effort to build a useful product while exploring the boundaries, benefits, and trade-offs of AI-aided development.
---
β¨ What Can This Thing Do?
Glad you asked! Here's the good stuff:
- π± **NFC & QR Code Entry** β Guests tap an NFC tag or scan a QR code to open the guestbook instantly β no app download needed!
- π§ **Multi-Step Wizard** β A beautiful 4-step form guides guests through leaving their entry (Basics β Favorites β Fun Facts β Message)
- πΈ **Photo Upload with Compression** β Guests snap a selfie or upload a photo, automatically compressed client-side for fast uploads
- π¨ **Polaroid-Style Cards** β Entries are displayed as gorgeous polaroid-style cards with handwritten fonts
- π **Dark Mode** β Full light/dark/system theme support with zero flash of unstyled content (FOUC)
- π **Multilingual** β English and German out of the box with `@nuxtjs/i18n`
- π₯οΈ **Slideshow Mode** β Full-screen auto-advancing slideshow, perfect for displaying on a TV at your event
- π **PDF Export** β Download your entire guestbook as a beautifully formatted PDF
- π **Admin Dashboard** β Password-protected admin panel for entry moderation (approve, reject, delete)
- π **Entry Moderation** β Three-state system: pending β approved / rejected β keep your guestbook clean!
- π **Offline Support** β Entries are queued in IndexedDB when offline and synced when back online
- π± **PWA Ready** β Install as a Progressive Web App on any device
- π³ **Docker Support** β Ready-to-use Dockerfile and docker-compose for easy self-hosting
---
# π± How It Works
The magic is simple β here's the flow:
```
1. π² Guest taps NFC tag or scans QR code
β
2. π Browser opens Tap & Tell (no app install!)
β
3. π§ 4-step wizard collects:
Step 1: Name + Photo (required)
Step 2: Favorites β color, food, movie, song, video (optional)
Step 3: Fun Facts β superpowers, hidden talents, preferences (optional)
Step 4: Personal Message (required)
β
4. πΎ Entry saved with photo compression
β
5. π Entry appears in the guestbook!
```
> [!NOTE]
> π Steps 1 (Basics) and 4 (Message) are required. Steps 2 (Favorites) and 3 (Fun Facts) are completely optional β guests can skip them!
---
# π Getting Started
Ready to set up your own digital guestbook? Let's go! π
## Prerequisites
- **Node.js** 18+ installed
- **pnpm** package manager (`npm install -g pnpm`)
## Quick Start
```bash
# 1. Clone the repo
git clone https://github.com/Disane87/tap-and-tell.git
cd tap-and-tell
# 2. Install dependencies
pnpm install
# 3. Start the dev server
pnpm dev
```
That's it! Open `http://localhost:3000` and you're running! π
## Environment Variables
Create a `.env` file in the project root:
```env
# PostgreSQL connection string
POSTGRES_URL=postgresql://user:password@localhost:5432/tapandtell
# JWT signing secret (CHANGE THIS in production!)
JWT_SECRET=your-jwt-secret-here
# CSRF token signing secret (CHANGE THIS in production!)
CSRF_SECRET=your-csrf-secret-here
# Master encryption key for photo encryption (64 hex chars, REQUIRED in production!)
ENCRYPTION_MASTER_KEY=
# Storage directory for entries and photos
DATA_DIR=.data
```
> [!CAUTION]
> β οΈ **Security First!** Always set secure values for `JWT_SECRET`, `CSRF_SECRET`, and `ENCRYPTION_MASTER_KEY` in production!
---
# π³ Docker Deployment
Prefer containers? We've got you covered!
### Docker Compose β Production (Recommended)
The `docker-compose.prod.yml` is a self-contained stack (app + PostgreSQL) ready for **Portainer** or any Docker host.
**1. Generate secrets**
All secrets must be set before first start. Generate them with `openssl`:
```bash
# JWT signing secret (64-char hex)
openssl rand -hex 32
# CSRF token secret (64-char hex)
openssl rand -hex 32
# Photo encryption master key (64-char hex)
openssl rand -hex 32
# PostgreSQL password (64-char hex)
openssl rand -hex 32
# API token secret (base64)
openssl rand -base64 32
```
**2. Configure environment variables**
Set the generated values in `docker-compose.prod.yml` or pass them as environment variables:
| Variable | Format | Description |
|---|---|---|
| `POSTGRES_PASSWORD` | 64-char hex | PostgreSQL password (same in `postgres` and `app` services) |
| `JWT_SECRET` | 64-char hex | JWT signing key for authentication |
| `CSRF_SECRET` | 64-char hex | CSRF double-submit cookie secret |
| `ENCRYPTION_MASTER_KEY` | 64-char hex | AES-256-GCM photo encryption key |
| `TOKEN_SECRET` | base64 string | API token signing secret |
| `DB_SSL` | `"false"` | Set to `"false"` for Docker-to-Docker connections (no SSL). Omit for external DBs (Neon, Supabase) where SSL is required. |
> [!CAUTION]
> Never commit secrets to version control. Use environment variables, Docker secrets, or Portainer's environment variable UI instead.
**3. Deploy**
```bash
# Direct Docker Compose
docker compose -f docker-compose.prod.yml up -d
```
Or in **Portainer**: Stacks β Add Stack β paste the compose file β set environment variables in the UI.
### Docker Compose β Development
```bash
docker compose up -d
```
### Standalone Docker
```bash
# Build the image
docker build -t tap-and-tell .
# Run the container
docker run -d \
-p 3000:3000 \
-e POSTGRES_URL=postgresql://user:password@host:5432/tapandtell \
-e DB_SSL=false \
-e JWT_SECRET=$(openssl rand -hex 32) \
-e CSRF_SECRET=$(openssl rand -hex 32) \
-e ENCRYPTION_MASTER_KEY=$(openssl rand -hex 32) \
-e TOKEN_SECRET=$(openssl rand -base64 32) \
-v tap-and-tell-data:/app/data \
tap-and-tell
```
> [!IMPORTANT]
> π Mount a volume to `/app/data` to persist your guestbook entries and photos across container restarts!
---
# π Pages & Features
Here's a tour of everything Tap & Tell offers:
## π Landing Page (`/`)
The main entry point for guests! Features:
- π **Swipeable Carousel** β Intro slide followed by existing entry slides
- π **Bottom Sheet Wizard** β The 4-step form slides up from the bottom
- β¨οΈ **Keyboard & Swipe Navigation** β Navigate entries with arrow keys or swipe gestures
- π± **NFC Context Detection** β Personalized welcome when entering via NFC tag
- π΅ **Pagination Dots** β Visual indicators for carousel position
## π Guestbook (`/guestbook`)
Browse all approved entries in a beautiful grid:
- π **Search by Name** β Debounced search (300ms) for instant filtering
- π **Sort Options** β Newest first or oldest first
- π **PDF Export** β Download the entire guestbook as a formatted PDF
- π₯οΈ **Slideshow Link** β Quick access to slideshow mode
- π **Detail View** β Click any card to see the full entry in a bottom sheet
## π₯οΈ Slideshow (`/slideshow`)
Perfect for displaying on a TV at your event!
- βΆοΈ **Auto-Advancing** β Configurable interval (3β30 seconds, default 8)
- βΈοΈ **Play/Pause Controls** β Take control when you want
- π₯οΈ **Fullscreen Mode** β True fullscreen for maximum impact
- β¨οΈ **Keyboard Controls** β Arrow keys, Space, P (pause), F (fullscreen), ESC (exit)
- π» **Auto-Hide Controls** β Controls fade away during playback
## π Admin Dashboard (`/admin`)
Manage your guestbook with a password-protected admin panel:
- π **Status Tabs** β Filter by All, Pending, Approved, Rejected
- β
**Bulk Actions** β Approve or reject multiple entries at once
- ποΈ **Individual Management** β Delete or change status of single entries
- π’ **Entry Counts** β See counts per status at a glance
- πͺ **Secure Logout** β Token-based session management
## π± QR Code Generator (`/admin/qr`)
Generate QR codes for your event:
- π― **Custom Event Name** β Embed your event name in the URL
- π₯ **Download Options** β Export as PNG or SVG
- π **Copy URL** β Quick copy to clipboard
- π **NFC-Compatible URLs** β Generates `?source=nfc&event=YourEvent` links
---
# ποΈ Architecture
Let's peek under the hood! Here's how Tap & Tell is built:
## Tech Stack
| Layer | Technology |
|-------|-----------|
| **Framework** | Nuxt 4.3 (SSR disabled β client-side SPA) |
| **UI Library** | Vue 3.5 with Composition API |
| **Styling** | Tailwind CSS v4 via `@tailwindcss/vite` |
| **Components** | shadcn-vue (headless UI) |
| **Language** | TypeScript 5.9 |
| **Icons** | Lucide Vue Next |
| **Database** | PostgreSQL 16+ with Row-Level Security (RLS) |
| **ORM** | Drizzle ORM |
| **Auth** | JWT (jose) + 2FA (TOTP / Email OTP) |
| **Encryption** | AES-256-GCM per-tenant photo encryption |
| **i18n** | @nuxtjs/i18n (EN + DE) |
| **PDF Generation** | jsPDF |
| **QR Codes** | qrcode |
| **Utilities** | VueUse |
| **Toasts** | vue-sonner |
| **PWA** | @vite-pwa/nuxt |
| **Package Manager** | pnpm |
| **Deployment** | Docker (self-hosted) |
## Project Structure
```
tap-and-tell/
βββ app/ # π₯οΈ Nuxt client application
β βββ pages/ # Route pages
β βββ components/ # Vue components
β β βββ form/ # Wizard form steps
β β βββ ui/ # shadcn-vue base components
β βββ composables/ # Vue composables (state & logic)
β βββ types/ # TypeScript type definitions
β βββ plugins/ # Nuxt client plugins
β βββ layouts/ # Page layouts
β βββ lib/ # Utility functions
β βββ assets/ # Static assets (CSS, images)
β
βββ server/ # βοΈ Nitro server
β βββ routes/api/ # API endpoints
β β βββ g/ # Public guest endpoints (flat routes)
β β βββ auth/ # Authentication + 2FA
β β βββ tenants/ # Tenant/guestbook management
β β βββ photos/ # Photo serving (encrypted)
β βββ database/ # Schema + migrations (Drizzle ORM)
β βββ utils/ # Server utilities (crypto, auth, storage)
β βββ plugins/ # Server startup plugins
β
βββ i18n/ # π Internationalization
β βββ locales/ # EN + DE translation files
β
βββ public/ # π Static public assets
β βββ icons/ # PWA icons
β
βββ plans/ # π Development plan documents
βββ nuxt.config.ts # βοΈ Nuxt configuration
βββ package.json # π¦ Dependencies
βββ tsconfig.json # π§ TypeScript config
```
## Storage Layer
Tap & Tell uses **PostgreSQL 16+** with Row-Level Security (RLS) for multi-tenant data isolation. Photos are stored on disk with **AES-256-GCM per-tenant encryption**.
```
PostgreSQL (via Drizzle ORM)
βββ users, sessions, user_two_factor # Auth & 2FA
βββ tenants, tenant_members # Multi-tenancy
βββ guestbooks, entries # Core data (RLS-protected)
βββ audit_logs, api_apps, api_tokens # Security & API access
.data/photos/
βββ [guestbookId]/[entryId].[ext] # AES-256-GCM encrypted photos
βββ ...
```
> [!NOTE]
> Photo storage is configurable via `STORAGE_DRIVER` (`local`, `vercel-blob`, or `s3`) and `DATA_DIR` (default: `.data/`).
---
# π API Reference
All API endpoints at a glance. Authenticated endpoints use HTTP-only JWT cookies with CSRF protection.
## Public β Guest Endpoints (No Auth)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/g/[id]/info` | Guestbook info (name, settings, type) |
| `GET` | `/api/g/[id]/entries` | Approved entries for a guestbook |
| `POST` | `/api/g/[id]/entries` | Create a new guest entry (rate-limited) |
| `GET` | `/api/photos/[tenantId]/[filename]` | Serve encrypted photo |
| `GET` | `/api/health` | Health check endpoint |
| `GET` | `/api/og` | Locale-aware OG image (`?lang=de\|en`) |
## Authentication & Profile
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/auth/login` | Login with email/password, set JWT cookies |
| `POST` | `/api/auth/register` | Register a new account |
| `POST` | `/api/auth/logout` | Clear auth cookies |
| `POST` | `/api/auth/refresh` | Refresh access token |
| `GET` | `/api/auth/me` | Get current user profile |
| `PUT` | `/api/auth/me` | Update name and/or email |
| `DELETE` | `/api/auth/me` | Delete account (requires password) |
| `PUT` | `/api/auth/password` | Change password |
| `GET` | `/api/auth/csrf` | Get CSRF token |
| `POST` | `/api/auth/avatar` | Upload avatar (multipart, max 5 MB) |
| `DELETE` | `/api/auth/avatar` | Delete avatar |
| `GET` | `/api/auth/avatar/[userId]` | Serve avatar image (public) |
## Two-Factor Authentication (2FA)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/auth/2fa/setup` | Start 2FA setup (returns QR code) |
| `POST` | `/api/auth/2fa/verify-setup` | Verify TOTP code to activate 2FA |
| `GET` | `/api/auth/2fa/status` | Check if 2FA is enabled |
| `POST` | `/api/auth/2fa/verify` | Verify 2FA code during login |
| `POST` | `/api/auth/2fa/disable` | Disable 2FA |
| `POST` | `/api/auth/2fa/resend` | Resend email OTP code |
## Tenant Management (JWT Required)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants` | List user's tenants |
| `POST` | `/api/tenants` | Create a new tenant |
| `GET` | `/api/tenants/[uuid]` | Get tenant details |
| `PUT` | `/api/tenants/[uuid]` | Update tenant settings |
| `DELETE` | `/api/tenants/[uuid]` | Delete tenant |
| `POST` | `/api/tenants/[uuid]/rotate-key` | Rotate encryption key |
## Guestbook Management (JWT Required)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/guestbooks` | List guestbooks with entry counts |
| `POST` | `/api/tenants/[uuid]/guestbooks` | Create a new guestbook |
| `GET` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Get guestbook details |
| `PUT` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Update guestbook settings |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]` | Delete guestbook (cascades entries) |
| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/header` | Upload header image |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/header` | Delete header image |
| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/background` | Upload background image |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/background` | Delete background image |
## Entry Moderation (JWT Required)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries` | All entries (admin view) |
| `PATCH` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/[id]` | Update entry status |
| `DELETE` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/[id]` | Delete an entry |
| `POST` | `/api/tenants/[uuid]/guestbooks/[gbUuid]/entries/bulk` | Bulk status update |
## Team Members (JWT Required)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/members` | List team members |
| `POST` | `/api/tenants/[uuid]/members/invite` | Invite team member |
| `GET` | `/api/tenants/[uuid]/members/invites` | List pending invites |
| `DELETE` | `/api/tenants/[uuid]/members/invites/[id]` | Cancel invite |
| `DELETE` | `/api/tenants/[uuid]/members/[userId]` | Remove team member |
| `GET` | `/api/invites/[token]` | Get invite details (public) |
| `POST` | `/api/invites/accept` | Accept team invite (public) |
## API Apps & Tokens (JWT Required)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/tenants/[uuid]/apps` | List API apps |
| `POST` | `/api/tenants/[uuid]/apps` | Create API app |
| `GET` | `/api/tenants/[uuid]/apps/[appId]` | Get API app details |
| `PUT` | `/api/tenants/[uuid]/apps/[appId]` | Update API app |
| `DELETE` | `/api/tenants/[uuid]/apps/[appId]` | Delete API app |
| `GET` | `/api/tenants/[uuid]/apps/[appId]/tokens` | List tokens |
| `POST` | `/api/tenants/[uuid]/apps/[appId]/tokens` | Create token |
| `DELETE` | `/api/tenants/[uuid]/apps/[appId]/tokens/[tokenId]` | Revoke token |
| `GET` | `/api/scopes` | List available API scopes |
## Analytics (JWT Required)
| Method | Endpoint | Description |
|--------|----------|-------------|
| `POST` | `/api/analytics/events` | Track analytics event |
| `GET` | `/api/tenants/[uuid]/analytics/overview` | Dashboard overview |
| `GET` | `/api/tenants/[uuid]/analytics/traffic` | Traffic analytics |
| `GET` | `/api/tenants/[uuid]/analytics/sources` | Traffic sources |
| `GET` | `/api/tenants/[uuid]/analytics/devices` | Device breakdown |
| `GET` | `/api/tenants/[uuid]/analytics/funnel` | Conversion funnel |
### Quick Start Example
```bash
# Login
curl -X POST /api/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "your-password"}'
# β Sets access_token and refresh_token cookies
# Create a guest entry (public, no auth)
curl -X POST /api/g/your-guestbook-id/entries \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"message": "What an amazing event!",
"photo": "data:image/jpeg;base64,..."
}'
```
---
# π§© Composables
The brain of Tap & Tell lives in these composables β each one handles a specific concern:
| Composable | What It Does |
|-----------|-------------|
| `useAuth()` | π JWT cookie-based authentication (login, register, logout, profile) |
| `useGuests()` | π CRUD operations for guestbook entries. Module-level shared state across the app |
| `useGuestForm()` | π§ 4-step wizard state management with per-step validation |
| `useGuestbook()` | π Public guestbook operations using flat `/api/g/[id]` endpoints |
| `useTenantAdmin()` | π Admin entry operations (fetch all, delete, update status, bulk) |
| `useTheme()` | π Light/dark/system theme with localStorage persistence & FOUC prevention |
| `useNfc()` | π± Detects NFC context from URL query params (`?source=nfc&event=...`) |
| `useSlideshow()` | π₯οΈ Auto-advancing slideshow with play/pause/fullscreen controls |
| `useEntryFilters()` | π Debounced search & sort for the guestbook page |
| `usePdfExport()` | π Multi-page PDF generation with photos, favorites, and fun facts |
| `useImageCompression()` | πΈ Client-side image compression (max 1920px, target 500KB) |
| `useOfflineQueue()` | π IndexedDB-based offline entry queuing with auto-sync |
---
# π¨ Theme System
Tap & Tell features a **3-layer theme initialization** to prevent any flash of unstyled content (FOUC):
```
Layer 1: Inline in <head>
ββ Runs BEFORE first paint
ββ Reads localStorage, applies `dark` class to <html>
ββ Zero visual flash! β‘
Layer 2: Client Plugin (theme.client.ts)
ββ Syncs reactive Vue state with DOM
ββ Listens for system preference changes
Layer 3: <ClientOnly> Wrapper
ββ ThemeToggle component only renders on client
ββ Prevents SSR hydration mismatches
```
Toggle between **Light** βοΈ, **Dark** π, and **System** π» modes with a single click.
---
# π Internationalization
All user-facing text is translatable β no hardcoded strings anywhere!
| Feature | Details |
|---------|---------|
| **Languages** | π¬π§ English (default) + π©πͺ Deutsch |
| **Strategy** | No URL prefix, browser detection |
| **Persistence** | Cookie `i18n_locale` |
| **Module** | `@nuxtjs/i18n` |
Translation files live in `i18n/locales/` covering all scopes: form, guestbook, admin, navigation, slideshow, toasts, and more.
---
# π± NFC & QR Code Setup
Setting up NFC tags or QR codes for your event is easy!
## NFC Tags
1. Get writable NFC tags (NTAG215 or similar)
2. Use any NFC writer app to write the URL:
```
https://your-domain.com/?source=nfc&event=YourEventName
```
3. Place tags at your event venue β guests tap and they're in! π²
## QR Codes
1. Go to `/admin/qr` in your admin panel
2. Enter your event name
3. Download as PNG or SVG
4. Print and display at your venue! π¨οΈ
> [!TIP]
> π‘ **Pro tip**: Use both NFC tags AND QR codes! NFC for quick access, QR as a fallback for phones without NFC support.
---
# π Data Model
Here's what a guest entry looks like under the hood:
```typescript
interface GuestEntry {
id: string // UUID
name: string // Guest's name
message: string // Personal message
photoUrl?: string // Photo path (e.g., /api/photos/{id}.jpg)
answers?: GuestAnswers // Optional form answers
createdAt: string // ISO 8601 timestamp
status?: EntryStatus // 'pending' | 'approved' | 'rejected'
rejectionReason?: string // Why entry was rejected
}
interface GuestAnswers {
// π¨ Favorites
favoriteColor?: string
favoriteFood?: string
favoriteMovie?: string
favoriteSong?: { title: string; artist?: string; url?: string }
favoriteVideo?: { title: string; url?: string }
// π Fun Facts
superpower?: string
hiddenTalent?: string
desertIslandItems?: string
coffeeOrTea?: 'coffee' | 'tea'
nightOwlOrEarlyBird?: 'night_owl' | 'early_bird'
beachOrMountains?: 'beach' | 'mountains'
// π Our Story
howWeMet?: string
bestMemory?: string
}
```
---
<details>
<summary><h2>β‘ Advanced Configuration</h2></summary>
### Image Compression Settings
Client-side image compression is applied automatically before upload:
| Setting | Value |
|---------|-------|
| Max dimension | 1920px |
| Target file size | 500KB |
| Initial JPEG quality | 0.8 |
| Minimum JPEG quality | 0.3 (adaptive) |
### PWA Configuration
Tap & Tell is a fully-configured Progressive Web App:
| Setting | Value |
|---------|-------|
| Display mode | Standalone |
| Orientation | Portrait |
| Theme color | Dark |
| Icon | SVG (any size, maskable) |
| Font caching | Google Fonts (1-year CacheFirst) |
| Offline | Navigate fallback to `/` |
### Server-Side Validation
Entries are validated server-side with these constraints:
| Field | Constraint |
|-------|-----------|
| `name` | Required, 1β100 characters |
| `message` | Required, 1β1000 characters |
| `photo` | Optional, max 7MB (base64) |
### Auth Token Details
| Property | Value |
|----------|-------|
| Algorithm | HS256 (JWT via `jose`) |
| Access Token | 15 minutes, HTTP-only cookie |
| Refresh Token | 7 days, HTTP-only cookie, stored in DB |
| CSRF | Double-submit cookie pattern |
| 2FA | TOTP (RFC 6238) + Email OTP |
</details>
---
# π οΈ Development
Want to contribute or customize? Here's how to get the development environment running:
## Commands
```bash
pnpm install # Install dependencies
pnpm dev # Start development server (https://localhost:3000)
pnpm build # Build for production
pnpm preview # Preview production build locally
pnpm exec nuxi typecheck # Run TypeScript type checking
```
## Key Architectural Decisions
Here are the "why"s behind the design:
| Decision | Reasoning |
|----------|-----------|
| **SSR Disabled** | Client-side SPA avoids hydration mismatches with localStorage, NFC APIs, and browser-only features |
| **Module-Level State** | Composables use module-level `ref()` instead of `useState()` to prevent SSR payload conflicts |
| **PostgreSQL + RLS** | Multi-tenant isolation via Row-Level Security, per-tenant encryption for photos |
| **JWT Cookies** | HTTP-only access (15min) + refresh (7d) tokens with CSRF protection |
| **Client-Side Compression** | Reduces upload size and server load β images compressed before sending |
| **IndexedDB Offline Queue** | Entries are never lost, even without internet β syncs automatically when back online |
| **3-Layer Theme Init** | Prevents FOUC completely β no flash between page load and theme application |
---
# π€ Contributing
Want to make Tap & Tell even better? That's awesome! π
Here's how to get started:
1. π΄ **Fork** the repository
2. πΏ **Create** a feature branch (`git checkout -b feature/amazing-feature`)
3. π» **Make** your changes
4. β
**Build** to verify (`pnpm build`)
5. π **Commit** with conventional commits (`feat: add amazing feature`)
6. π **Push** and open a Pull Request
### Guidelines
- π€ **Code comments & JSDoc** in English
- π **All user-facing text** must use i18n translation keys
- π¨ **Styling** with Tailwind CSS utility classes
- π§© **UI components** follow shadcn-vue conventions
- βΏ **Accessibility** (a11y) best practices
- π **TypeScript** β avoid `any` type
- π **Security** β no hardcoded secrets, validate at boundaries
### Commit Convention
We use [Conventional Commits](https://www.conventionalcommits.org/):
```
feat: add new feature
fix: resolve a bug
docs: update documentation
refactor: restructure code
style: formatting changes
test: add or update tests
chore: maintenance tasks
```
---
# π Security Notes
> [!CAUTION]
> **Before going to production, make sure to:**
> - π Set a secure `JWT_SECRET` (not the default)
> - π Set a secure `CSRF_SECRET` (not the default)
> - π Generate a 64-character hex `ENCRYPTION_MASTER_KEY` for photo encryption
> - π All admin features require 2FA (TOTP or Email OTP)
---
# π‘ Use Case Ideas
Here are some creative ways to use Tap & Tell:
- π **Weddings** β Let guests leave their wishes and photos for the couple
- π **Birthday Parties** β Collect fun facts and memories from attendees
- π’ **Corporate Events** β Gather feedback and networking connections
- π **Graduations** β Classmates share their favorite memories
- π **Holiday Parties** β Guests share their holiday traditions and wishes
- π **Housewarming** β Visitors leave advice and well-wishes for the new home
- πΈ **Concerts & Festivals** β Fans share their experience and favorite moments
---
# π That's a Wrap!
Thanks for checking out **Tap & Tell**! If you find it useful, give it a β on GitHub β it really helps! π
Got a bug to report? Have an idea for a new feature? [Open an issue](https://github.com/Disane87/tap-and-tell/issues) and let's make this better together! π
---
<p align="center">
Made with β€οΈ using <a href="https://nuxt.com/">Nuxt</a>, <a href="https://vuejs.org/">Vue</a>, and <a href="https://tailwindcss.com/">Tailwind CSS</a>
</p>