DEV Community

Cover image for Building 1000 Candles: A Digital Memorial That Taught Me About Scale, Optimization, and Remembrance
Chris
Chris

Posted on

Building 1000 Candles: A Digital Memorial That Taught Me About Scale, Optimization, and Remembrance

When I started building 1000 Candles for the Kiroween Hackathon, I thought I was just creating a cool 3D visualization. What I ended up with was something much more meaningful. A digital space where people can light virtual candles in memory of loved ones, and a deep dive into the challenges of building real-time, scalable web applications.

The Concept: A Graveyard of Light

The idea was simple but powerful. Create a haunting 3D graveyard where users can light candles with personal dedications. Each candle would glow with realistic flame physics, positioned among hundreds of others, creating a collective memorial that grows over time.

But as any developer knows, simple ideas rarely stay simple.

The Tech Stack: Choosing the Right Tools

I built 1000 Candles with Next.js 14, Three.js, and React Three Fiber for the 3D rendering. I wrote custom GLSL shaders for the flame effects because I wanted them to look really realistic. For the database, I used SQLite locally and Turso for production since it works great with serverless. Tailwind CSS handled the styling, and I added Tone.js for spatial audio to make it more immersive.

The choice of Three.js was obvious. I needed real-time 3D rendering with good performance. But the database choice? That's where things got interesting.

Challenge #1: The Polling Problem

Users needed to see new candles appear in real-time. My first approach? Poll the database every 5 seconds.

With 100 concurrent users, that's 1,200 requests per minute, or 72,000 requests per hour. Each request fetches 200+ candle records. That's 14.4 million row reads per hour!

On Turso, that would cost around $20 per month. Not terrible, but not great for a hackathon project.

The Optimization Journey

I explored several solutions. Server-Sent Events would be real-time but keeps serverless functions alive which costs money on Vercel. WebSockets would be perfect but they're not supported on Vercel serverless. Third-party services like Pusher or Ably are great but cost $29 to $49 per month.

None of these felt right for a memorial site that should be simple and sustainable.

Here's what I built. A metadata table with a single row storing the last candle timestamp. A lightweight endpoint that returns just this timestamp. localStorage to cache the last known timestamp. And smart polling that only fetches full data when the timestamp changes.

Challenge #2: Making It Beautiful

The technical challenges were one thing, but this was a memorial site. It needed to feel special.

Custom GLSL Shaders

I wrote custom vertex and fragment shaders for the candle flames:

// Flickering flame effect
float flicker = sin(time * 3.0 + position.y * 2.0) * 0.1;
float glow = 1.0 - length(uv) * 0.5;
vec3 flameColor = mix(
  vec3(1.0, 0.4, 0.0),  // Orange
  vec3(1.0, 0.9, 0.3),  // Yellow
  glow + flicker
);
Enter fullscreen mode Exit fullscreen mode

Each flame flickers independently, creating a mesmerizing effect when you see hundreds of them together.

Particle Systems

I added atmospheric effects. Fireflies drifting through the scene. Falling leaves for autumn ambiance. Floating ash particles. Ground fog using volumetric techniques. And stars twinkling in the background.

Spatial Audio

Using Tone.js, I implemented spatial audio. Ambient wind sounds. Crackling fire positioned at each candle. Distant owl hoots. Rustling leaves. The audio responds to camera position, creating an immersive 3D soundscape.

Lesson learned: Technical excellence means nothing if the experience doesn't move people.

Challenge #3: Content Moderation

This is a public memorial. I needed to ensure respectful content without manual review.

I integrated Pollinations AI for content moderation:

async function moderateContent(dedication: string, from: string) {
  const prompt = `Is this memorial dedication appropriate and respectful?
  Dedication: "${dedication}"
  From: "${from}"

  Respond with YES or NO and a brief reason.`;

  const response = await fetch('https://text.pollinations.ai/', {
    method: 'POST',
    body: prompt,
  });

  // Parse AI response and validate
}
Enter fullscreen mode Exit fullscreen mode

The system checks for inappropriate content and validates against spam patterns. If the AI is down, it fails open and allows the content through. Better to let something through than block legitimate memorials. It also provides helpful error messages when something is rejected.

Lesson learned: AI moderation isn't perfect, but it's a good first line of defense.

Challenge #4: Rate Limiting

To prevent abuse, I implemented server-side rate limiting:

// User sessions table
CREATE TABLE user_sessions (
  user_fingerprint TEXT PRIMARY KEY,
  last_candle_lit_at INTEGER,
  daily_candle_count INTEGER,
  last_reset_date TEXT
);
Enter fullscreen mode Exit fullscreen mode

I use browser fingerprinting which is privacy-conscious. Daily limits are configurable via environment variables. Everything resets automatically at midnight. And when users hit the limit, they get helpful error messages showing exactly when they can light another candle.

Lesson learned: Rate limiting is essential, but make it transparent and user-friendly.

The User Experience

When you visit 1000 Candles, you see an intro sequence with typing animation. Then the 3D graveyard appears with a blood moon and atmospheric effects. Click "Light a Candle" to open the ritual modal. Enter your dedication and name. Watch your candle appear with a smooth camera animation. You can explore other candles with hover tooltips. And search for specific memorials by pressing F.

The experience is designed to be respectful with somber colors and gentle animations. It's immersive with the 3D environment and spatial audio. It's accessible with keyboard shortcuts and mobile-friendly controls. And it's performant, running at 60fps even with over 1000 candles.

Performance Optimizations

To render 1000+ candles at 60fps, I implemented:

Frustum Culling

Only render candles visible to the camera:

const frustum = new THREE.Frustum();
frustum.setFromProjectionMatrix(camera.projectionMatrix);

candles.forEach(candle => {
  if (frustum.containsPoint(candle.position)) {
    // Render this candle
  }
});
Enter fullscreen mode Exit fullscreen mode

Level of Detail (LOD)

Reduce geometry for distant candles:

const distance = camera.position.distanceTo(candle.position);
const geometry = distance > 50 
  ? lowPolyGeometry 
  : highPolyGeometry;
Enter fullscreen mode Exit fullscreen mode

Instancing

Reuse geometries for multiple candles:

const instancedMesh = new THREE.InstancedMesh(
  geometry,
  material,
  candleCount
);
Enter fullscreen mode Exit fullscreen mode

Result: Smooth 60fps with over 1000 candles on mid-range hardware.

What I Learned

Technical Stuff

Abstraction is powerful. The database adapter saved me countless hours. I learned to optimize for the common case because 99% of polls don't need full data. I also learned to measure before optimizing because I almost over-engineered the solution. Serverless has constraints but they force you to think differently. And localStorage is seriously underrated. It's simple, fast, and works everywhere.

Design Stuff

Performance is a feature. Users notice lag, especially in 3D. Atmosphere matters. The audio and particles make it special. You have to respect the subject. This is a memorial, not a game. Accessibility counts. Keyboard shortcuts and mobile support aren't optional. And less is more. I removed features that didn't serve the core experience.

Personal Stuff

Scope creep is real. I almost added multiplayer cursors which would have been cool but totally unnecessary. Deadlines help. The hackathon forced me to ship instead of endlessly tweaking. Community feedback is gold. Early testers found issues I completely missed. Documentation matters. Future me will thank present me. And most importantly, have fun. This was a joy to build.

The Numbers

After launch:

  • 60fps average framerate
  • 99.5% reduction in database reads
  • $0.10/month database costs
  • 0 downtime on Vercel
  • Countless memories preserved

Try It Yourself

Visit 1000candles.online and light a candle for someone you remember.

The code is open source: github.com/chrisbuildsonline/1000candles

What's Next?

I'm considering:

  • Candle colors - Let users choose flame colors
  • Shared candles - Multiple people can contribute to one candle
  • Export memories - Download a screenshot of your candle
  • Seasonal themes - Different atmospheres for holidays
  • Mobile app - Native iOS/Android with AR features

But for now, I'm happy with what it is: a simple, beautiful space for remembrance.

Final Thoughts

Building 1000 Candles taught me that the best projects are the ones that solve real problems, even if that problem is just "I want a beautiful place to remember someone."

The technical challenges were fun to solve, but what matters most is that people are using it. They're lighting candles for grandparents, friends, pets, and even abstract concepts like "hope" and "peace."

That's the magic of the web: you can build something meaningful, deploy it globally, and watch it touch lives you'll never meet.

If you're working on a hackathon project, remember:

  • Start with why - What problem are you solving?
  • Ship early - Perfect is the enemy of done
  • Optimize later - But measure first
  • Make it beautiful - People remember how it feels
  • Have fun - That's what hackathons are for

Thank you for reading, and thank you to the Kiroween Hackathon organizers for the inspiration.

Now go light a candle. 🕯️


Top comments (0)