After years of thinking about it (since 2019!), I finally built a comprehensive family electronic bulletin board. It's displayed on a large LCD screen in our mud room, giving everyone a quick glance at what's happening in our lives.
The Vision
I wanted a centralized place where our whole family could see:
- Upcoming events and appointments - Never miss a soccer game or doctor's appointment
- To-do lists - Shared family tasks with priority levels
- Grocery list - Real-time sync so anyone can add items
- Weather - Quick glance at today's forecast
- Important notes - WiFi passwords, emergency contacts, etc.
- Countdowns - Excitement for upcoming vacations or events
- Birthday reminders - Never forget a family birthday
- Family messages - Quick announcements and celebrations
- Affirmations - Daily inspiration and motivation
Tech Stack
I built this as part of my existing Next.js app, keeping everything in one codebase:
- Next.js 16 with App Router - Server and client components
- SQLite (better-sqlite3) - Local database, simple and fast
- Tailwind CSS 4 - Responsive styling with custom color theme
- Clerk - Authentication for admin pages
- Open-Meteo API - Free weather data
- TypeScript - Type safety throughout
Architecture
Database Schema
The bulletin board uses 9 core tables in SQLite:
- bulletin_events - Calendar events with optional Google Calendar sync
- bulletin_todos - Task management with priority and assignment
- bulletin_grocery_items - Organized by category (produce, dairy, etc.)
- bulletin_notes - Permanent information (WiFi, contacts, etc.)
- bulletin_countdowns - Event countdowns with custom icons
- bulletin_birthdays - Auto-calculates days until next birthday
- bulletin_affirmations - Rotating daily inspiration
- bulletin_meals - Weekly meal planning
- bulletin_chores - Task assignment with frequency tracking
- bulletin_messages - Family announcements with expiration dates
Each table includes:
user_idfor tracking who created items- Timestamps (
created_at,updated_at) - Indexes on frequently queried columns
- SQLite CHECK constraints for enum values
API Endpoints
Following RESTful patterns:
Public (No Auth):
GET /api/bulletin/kiosk- Aggregates all data for kiosk displayGET /api/bulletin/weather- Proxies Open-Meteo API
Protected (Clerk Auth):
GET/POST /api/bulletin/events- List and create eventsGET/PATCH/DELETE /api/bulletin/events/[id]- Manage individual eventsGET/POST /api/bulletin/todos- List and create todosGET/PATCH/DELETE /api/bulletin/todos/[id]- Manage individual todosGET/POST /api/bulletin/grocery- List and create grocery itemsGET/PATCH/DELETE /api/bulletin/grocery/[id]- Manage individual items
All endpoints follow the same pattern:
- Validate authentication with Clerk
- Validate request data
- Use database helper functions from
lib/db.ts - Return consistent JSON responses
- Proper HTTP status codes (400, 401, 404, 500)
Kiosk Display
The kiosk page (/bulletin) is public - no authentication required. It:
- Fetches all data from
/api/bulletin/kioskaggregation endpoint - Auto-refreshes every 30 seconds
- Updates clock every second for real-time feel
- Uses responsive grid layout (1-3 columns based on screen size)
- Shows/hides widgets based on data availability
Widget Architecture:
I created a reusable WidgetContainer component that provides:
- Consistent header styling with gradient background
- Optional icons and loading states
- Error handling
- Standardized padding and spacing
Individual widgets:
- EventsWidget - Upcoming events with dates, times, locations
- TodosWidget - Active tasks with priority color coding
- GroceryWidget - Items grouped by category
- WeatherWidget - Current temp, high/low, weather icons
- MessagesWidget - Family announcements with priority levels
- CountdownWidget - Days until events with custom icons
- BirthdaysWidget - Upcoming birthdays with "TODAY!" highlighting
Admin Interface
The admin section (/admin/bulletin) requires authentication:
- Dashboard shows quick stats and links to management pages
- Todo management page (
/admin/bulletin/todos) has:- List view with checkboxes to mark complete
- Inline form for creating new todos
- Priority indicators (red/orange/gray)
- Delete functionality
- Real-time updates after actions
Following the same pattern, I can easily add admin pages for:
- Events management
- Grocery list management
- Notes/settings
- Message board
- Photo uploads
Key Features Implemented
1. Smart Todo Management
- Priority levels (high/medium/low) with color coding
- Assignment to specific family members
- Due dates with visual indicators
- Checkbox completion with timestamp tracking
- Filters for active vs. completed tasks
2. Real-Time Weather
- Open-Meteo API (free, no API key required)
- Current temperature and conditions
- High/low for the day
- Weather icon mapping for visual clarity
- Auto-refresh every 10 minutes
3. Category-Based Grocery List
- Organized by category (produce, dairy, meat, etc.)
- Quantity tracking
- Optional notes per item
- Checkbox to mark items as obtained
- Unchecked items display on kiosk
4. Event Calendar
- Start/end datetime support
- All-day event option
- Location field
- Custom color per event
- "Today" highlighting
- Ready for Google Calendar sync (future enhancement)
5. Birthday Countdown
- Calculates days until next birthday
- Handles year rollovers correctly
- Special "TODAY!" badge for today's birthdays
- "Tomorrow" indicator
- Relationship field (family/friend)
6. Family Messages
- Priority levels (low/normal/high)
- Message types (announcement/reminder/celebration)
- Expiration dates
- Author name display
- Icon mapping based on type
- Auto-hides after expiration
7. Countdown Timers
- Custom event countdowns
- Optional emoji/icon
- Custom colors
- Description field
- Days until calculation
- Hides after event passes
Code Patterns
Database Helper Functions
All database operations are centralized in lib/db.ts:
export function getBulletinTodos(completed?: boolean) {
const db = getDatabase();
let query = 'SELECT * FROM bulletin_todos WHERE 1=1';
const params: any[] = [];
if (completed !== undefined) {
query += ' AND completed = ?';
params.push(completed ? 1 : 0);
}
query += ` ORDER BY
CASE priority
WHEN 'high' THEN 1
WHEN 'medium' THEN 2
ELSE 3
END,
due_date ASC NULLS LAST,
sort_order ASC`;
return db.prepare(query).all(...params);
}This pattern:
- Provides type-safe access to database
- Handles SQL parameter binding
- Includes smart sorting (priority, then due date)
- Easy to test and maintain
API Route Pattern
Every API route follows this structure:
export async function POST(request: NextRequest) {
try {
// 1. Check authentication
const { userId } = await auth();
if (!userId) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// 2. Parse and validate request body
const body = await request.json();
const { title, description, priority, due_date } = body;
if (!title) {
return NextResponse.json(
{ error: "Title is required" },
{ status: 400 }
);
}
// 3. Call database helper function
const id = createBulletinTodo({
user_id: userId,
title,
description,
priority: priority || "medium",
due_date,
});
// 4. Return success response
return NextResponse.json({ ...todo, id }, { status: 201 });
} catch (error: any) {
console.error("Create todo error:", error);
return NextResponse.json(
{ error: error.message || "Failed to create todo" },
{ status: 500 }
);
}
}Widget Component Pattern
Widgets are simple, focused React components:
interface TodosWidgetProps {
todos: BulletinTodo[];
}
export default function TodosWidget({ todos }: TodosWidgetProps) {
if (!todos || todos.length === 0) {
return (
<WidgetContainer title="To-Do List" icon="✓">
<p className="text-slate-500 text-center py-4">No active tasks</p>
</WidgetContainer>
);
}
return (
<WidgetContainer title="To-Do List" icon="✓">
<div className="space-y-3">
{todos.map((todo) => (
// Render todo item...
))}
</div>
</WidgetContainer>
);
}This keeps widgets:
- Independent and reusable
- Easy to show/hide based on data
- Consistent styling via WidgetContainer
- Simple to test
Deployment Considerations
For a kiosk display, you'll want to:
-
Set up the display device
- Use a Raspberry Pi, old laptop, or dedicated display
- Configure auto-start of browser in kiosk mode
- Disable sleep/screensaver
- Set up auto-refresh or use SSE for real-time updates
-
Network considerations
- Ensure stable WiFi connection
- Consider static IP for easy access
- Optionally restrict bulletin board to home network
-
Browser settings
- Use Chrome/Firefox kiosk mode
- Hide scrollbars with CSS
- Disable context menus
- Prevent accidental navigation
-
Database backups
- SQLite database is a single file (
data/app.db) - Easy to backup with cron job
- Consider daily snapshots
- SQLite database is a single file (
Future Enhancements
The foundation is now in place for many enhancements:
- Google Calendar Sync - Automatically pull in family events
- Photo Slideshow - Rotate through family photos
- Meal Planning - Weekly meal calendar linked to grocery list
- Chore Rotation - Automatic chore assignments
- Real-time Updates - Server-Sent Events for instant updates
- Mobile Companion App - Native mobile UI for easier data entry
- Voice Integration - "Hey Google, add milk to grocery list"
- QR Code WiFi - Generate QR code for guest WiFi access
- Transit Info - Show commute times or bus schedules
- Package Tracking - Amazon/UPS delivery notifications
Lessons Learned
Keep it simple: I resisted the urge to build everything at once. Starting with todos, events, and grocery list gave me a working MVP. Other features can be added incrementally.
Separation of concerns: Database functions, API routes, and UI components are all separate. This makes testing and extending much easier.
Use existing infrastructure: Building this into my existing Next.js app meant I didn't need to set up new hosting, authentication, or deployment. It just works.
SQLite is underrated: For a family app with a few users, SQLite is perfect. No separate database server to maintain, backups are simple file copies, and it's blazing fast.
Public kiosk, authenticated admin: This pattern works great. Anyone in the family can see the bulletin board, but only authenticated users can manage content.
Conclusion
After 6 years of having this on my todo list, it's incredibly satisfying to finally have a working family bulletin board. The kids love seeing their tasks and upcoming events, and we use the grocery list constantly.
Total implementation time: ~4 hours Lines of code: ~2,500 Database tables: 10 API endpoints: 12 React components: 10
The best part? It's all just TypeScript, React, and SQL - technologies I already know. No new frameworks to learn, no complicated setup. Just good old web development solving a real family problem.
Now every time we walk through the mud room, we're informed, organized, and connected.
Try it yourself: Visit /bulletin to see the live demo, or check out the admin panel if you're logged in.
Built with Claude Code in December 2025
