Skip to content
Revianto
Back to portfolio

Undangoo: building a privacy-first wedding invitation product, end to end

Overview

Undangoo is a digital wedding invitation product I designed, built, and launched on my own. A couple creates an invitation, sends each guest a personal link, and guests open it and RSVP. I was the only engineer: product decisions, the Go API, three web apps, infrastructure, and release.

The problem

Digital invitations in Indonesia are usually a template and a shared link in a WhatsApp group. That works, but it has three weaknesses I wanted to design out:

  • Guest privacy. One public link means anyone can open it, forward it, or guess others. Guest names and RSVPs end up readable by people who were never invited.
  • Generic design. Most templates look alike and are built for the couple's tastes, not for being a polished, full web experience on a phone.
  • No control for the couple. Couples cannot see who opened their link, who replied, or who still needs a reminder without chasing people by hand.

Constraints

  • One engineer, so every system had to be simple enough to run and fix alone.
  • Guests open invitations on mid-range phones over mobile data, so pages had to be light and fast.
  • Personal data (names, phone numbers, RSVPs) had to be protected from the first release, not added later.

What I built

Landing & guest viewCustomer panelAdmin appPostgreSQLRedis & job queueObject storage + CDNEmail serviceGo API
High-level architecture

One personal link per guest. Each guest gets a unique token. There are no public, guessable invitation URLs, so there is nothing to enumerate. Opening the link records that the guest viewed it; replying records the RSVP.

A Go API with clear layers. The backend is a Go service (Fiber) on PostgreSQL, with Redis for rate limiting and a job queue for email, organised into routes, services, and models, with transactions wrapped consistently so a failed step never leaves half-written data. Invitation content lives in a typed JSON configuration, so adding a template means adding a design, not a new data model.

A flexible query layer. I designed the data access layer so queries can be composed instead of being one fixed block per model. Search conditions are a tree: AND and OR groups can sit inside each other to any depth, so a filter such as city = 'yogyakarta' AND (regency = 'sleman' OR regency = 'bantul') AND price = 100000 keeps its meaning, with the brackets where they belong. Every field and operator is checked against an allow-list for that model, and values are passed as parameters. A UNION across models lives in the repository layer, so each model can still be used on its own. Each model keeps its default select, join and where for the main data of an endpoint, and smaller functions, such as a validation that reads from the database, can reuse the same model and take only the parts they need, for example just the join and the where. This came from limits I ran into in earlier projects.

Four separate roles, kept apart in code. The platform admin, the customer who owns an invitation, the guest, and the anonymous public each have their own routes and checks. Customers must have an account with a verified email; guests never need one.

Three web apps on Next.js. One app for the landing page, the guest view, and the customer panel; a separate admin app, so admin features are never shipped to customer browsers. I used a written design system per area (landing, customer panel, owner dashboard, each premium template) so new screens stay consistent.

Free and premium templates. Free templates ship with every plan. Premium templates add themed motion and richer layouts, gated on the server, not only hidden in the interface.

Undangoo's template gallery page showing four of the twelve wedding invitation templates, each with a demo link.
  • Opening screen of a watercolor teal wedding invitation template with two birds on blossoms, the couple's names, and an Open Invitation button.
  • Opening screen of a deep red wedding invitation template with roses, a monogram, the couple's names, and an Open Invitation button.
  • Opening screen of a dark wedding invitation template with a road and hills in the background, bold lettering for the couple's names, and an Open Invitation button.
  • Opening screen of a dark green wedding invitation template with a gold monogram, olive branches, the couple's names, and a button to open the letter.
Demo screens, fictional couple and guest data

Security challenges

Because the product holds personal data, I treated security as part of the feature list:

  • Sign-in for the platform admin needs a one-time code sent by email, on top of the password, and new devices are recorded.
  • One-time codes have attempt limits, so they cannot be brute-forced.
  • Passwords are checked against a strength policy, with one shared rule and shared test cases so the server and both web apps agree.
  • Public forms use bot protection and honeypot fields.
  • Roles are isolated at the data layer. I reviewed this isolation, fixed the access-control issues found during review, and covered them with tests.
  • Personal data has a retention rule: an invitation and its guest data are permanently deleted 30 days after the plan expires, and the customer sees a warning with the exact date before that happens.

I also keep payment handling out of this write-up on purpose, and no secrets live in the repository; configuration comes from the environment.

Delivery and quality

  • Release: the web apps and the API deploy automatically from the main branch through CI. Containers run the services, and static assets are served from object storage behind a CDN.
  • Backups and alerts: database backups run daily to separate storage, and failures alert me on my phone.
  • Testing: unit tests on the backend, and end-to-end tests that run in a real Chrome browser. A feature only counts as tested after the end-to-end suite passes there.
  • Written decisions: product plan, current implementation, and product truths are kept in three synchronised documents, and open tasks live in one tracker. For a one-person project this is what keeps decisions from being lost.

Result

  • Undangoo is live in production at undangoo.com, with free and paid plans, an admin app, and 12 templates across the free and premium tiers.
  • Public marketing has started. I do not quote usage numbers yet, because the product is new and I would rather publish figures that are real than ones that look big.
  • The backend and infrastructure are small enough that one person can operate and change them safely, which was the main engineering goal.

What I learned

  • Designing privacy in from the start (one personal link per guest, strict roles) was much cheaper than retrofitting it.
  • A single source of truth for rules shared by several apps, such as the password policy, prevents the quiet drift that causes bugs.
  • State that belongs to one request must stay with that request. In an earlier project on Gin the context was read from a global *gin.Context, which caused duplicate responses; in Fiber it is passed explicitly down the layers, so that bug could not happen, and when I met the same problem in the earlier project I knew how to fix it.
  • For a solo product, written decisions and automated tests matter more than any single technology choice.

Stack

Go, Fiber, PostgreSQL, Redis, Next.js, Docker, Cloudflare R2, GitHub Actions.

Contact me
View CV