Files
wedding-website/plans/wedding-bootstrap-plan.md
Raul Lugo 4ced446c9c
Build & Deployment / Build & Deploy (push) Failing after 5s
initial commit
2026-08-10 17:55:26 +02:00

19 KiB

Wedding Website Bootstrap Plan

Goal

Build the dynamic foundation for a wedding website before final visual/content design is complete.

The website should support:

  • A public wedding homepage with static sections and personalized guest content.
  • Unique invitation links for each invited group/household.
  • RSVP and per-person attendance/meal collection.
  • Optional guest contributions such as song requests, toast requests, messages, and one photo upload.
  • A private /admin portal restricted to the couple through Google login.
  • A Docker Compose runtime with PostgreSQL and MinIO.

Confirmed decisions

Application stack

  • Framework: Next.js App Router.
  • Database: PostgreSQL.
  • Query layer: Kysely, no ORM.
  • Authentication: Google OAuth.
  • Admin access: restricted by email allowlist from .env.
  • Object storage: MinIO using S3-compatible APIs.
  • Deployment/runtime style: Docker Compose.
  • Admin UI component system: shadcn/ui only.

Admin UI requirement

All UI components used in the admin area must come from shadcn/ui.

This applies to every admin page under:

/admin
/admin/*

The admin area must use shadcn/ui components for:

  • layout primitives where applicable
  • cards
  • buttons
  • forms
  • inputs
  • selects
  • checkboxes
  • dialogs
  • dropdown menus
  • tables
  • badges
  • tabs
  • alerts
  • toasts/sonner notifications
  • action menus
  • empty states where applicable
  • loading/skeleton states

Custom admin UI should only be composed from shadcn/ui components and small local wrappers around them. Do not introduce another component library for the admin portal.

If a needed admin component is not yet installed from shadcn/ui, add it through the shadcn workflow and then reuse it consistently.

The guest-facing invitation pages may have custom wedding-themed design, but the admin management UI must remain shadcn-based.

Invitation model

  • The primary guest-facing unit is an Invitation, not an individual guest.
  • An invitation represents a household, couple, family, individual guest, or group.
  • Each invitation has one unique private token and one invite URL:
/invite/[token]
  • Each invitation must have a required language.
  • The invitation language is the source of truth for which language the guest-facing invitation page uses.
  • This is a core requirement because many guests may only speak one of the wedding languages.
  • Admins choose the invitation language when creating or editing an invitation.
  • Admin pages can remain in the couple's preferred management language initially, but guest-facing copy must respect the invitation language.

Invitation members

  • Each invitation contains one or more InvitationMember records.
  • Members are modeled from the start because attendance and food choices can differ per person.
  • Per-person attendance lives on invitation_members.attending.
  • The response table must not duplicate attendance status.

RSVP/response model

  • invitation_members owns per-person answers:
    • attending
    • meal preference
    • dietary restrictions
  • invitation_responses owns group-level logistics:
    • submitted timestamp
    • travel dates
    • stay location
    • transport help
    • message to the couple
  • RSVP edits are allowed until a configured deadline.

Photo uploads

  • One image per invitation/group.
  • The image may be replaced until the deadline.
  • Store files in MinIO.
  • Store object metadata in PostgreSQL.

Optional wedding interactions

Include these from the bootstrap if feasible:

  • Song requests.
  • Optional toast/speech request.
  • Advice, wish, or favorite memory message.

Proposed environment variables

DATABASE_URL=postgres://wedding:wedding@localhost:5432/wedding

AUTH_SECRET=change-me
AUTH_GOOGLE_ID=change-me
AUTH_GOOGLE_SECRET=change-me
ADMIN_EMAILS=person-one@example.com,person-two@example.com

APP_URL=http://localhost:3000
RSVP_DEADLINE=2026-08-01

S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_BUCKET=wedding-uploads
S3_ACCESS_KEY_ID=minioadmin
S3_SECRET_ACCESS_KEY=minioadmin
S3_FORCE_PATH_STYLE=true

Target folder structure

app/
  page.tsx
  invite/
    [token]/
      page.tsx
  admin/
    page.tsx
    guests/
      page.tsx
      new/
        page.tsx
      [id]/
        page.tsx
  api/
    auth/
      [...nextauth]/
        route.ts
    uploads/
      presign/
        route.ts
      complete/
        route.ts

lib/
  auth.ts
  config.ts
  db/
    client.ts
    types.ts
    queries/
      invitations.ts
      members.ts
      responses.ts
      uploads.ts
      song-requests.ts
      toast-requests.ts
      guest-messages.ts
  storage/
    s3.ts
    uploads.ts
  i18n/
    config.ts
    dictionaries/
      en.ts
      es.ts
      de.ts
  validation/
    invitation.ts
    response.ts
    upload.ts
    interactions.ts

scripts/
  migrate-sql.mjs

migrations/
  0001_initial_schema.sql

docker-compose.yml
plans/
  wedding-bootstrap-plan.md

Database schema plan

invitations

Represents one invite/group/household.

id uuid primary key
token text unique not null
display_name text not null
language text not null
contact_email text
contact_phone text
max_party_size integer not null default 1
allow_member_edits boolean not null default false
notes text
created_at timestamptz not null
updated_at timestamptz not null

Notes:

  • token must be unguessable.
  • display_name is what the guest sees, e.g. Ana & Luis or The Garcia Family.
  • language controls guest-facing page copy and form labels for this invitation.
  • Recommended initial language values should be short locale codes such as en, es, de, etc.
  • allow_member_edits allows flexible +1 or family edits later.

invitation_members

Represents each individual person under an invitation.

id uuid primary key
invitation_id uuid not null references invitations(id) on delete cascade
name text not null
is_primary_contact boolean not null default false
attending boolean
meal_preference text
dietary_restrictions text
notes text
created_at timestamptz not null
updated_at timestamptz not null

Notes:

  • attending = null means pending/unanswered.
  • Attendance status is derived from this table, not from invitation_responses.
  • Meal preference examples: meat, fish, vegetarian, vegan, kids, other.

invitation_responses

Stores group-level RSVP metadata and logistics.

id uuid primary key
invitation_id uuid unique not null references invitations(id) on delete cascade
submitted_at timestamptz
arrival_date date
departure_date date
stay_location text
coming_from text
needs_transport_help boolean
message_to_couple text
created_at timestamptz not null
updated_at timestamptz not null

Notes:

  • No attendance status here.
  • No attendee count here.
  • Admin dashboard derives totals from invitation_members.

photo_uploads

Stores one uploaded activity/surprise image per invitation.

id uuid primary key
invitation_id uuid unique not null references invitations(id) on delete cascade
bucket text not null
object_key text not null
original_filename text
content_type text
size_bytes integer
created_at timestamptz not null
updated_at timestamptz not null

Notes:

  • Unique invitation_id enforces one photo per invitation.
  • Replacing a photo updates this record and may delete/overwrite the old object.

song_requests

Stores optional party song requests.

id uuid primary key
invitation_id uuid not null references invitations(id) on delete cascade
title text not null
artist text
note text
created_at timestamptz not null

Notes:

  • Recommended limit: up to 3 song requests per invitation.

toast_requests

Stores optional requests to say a few words.

id uuid primary key
invitation_id uuid not null references invitations(id) on delete cascade
member_id uuid references invitation_members(id) on delete set null
speaker_name text not null
estimated_minutes integer
note text
status text not null default 'pending'
created_at timestamptz not null
updated_at timestamptz not null

Recommended statuses:

pending
accepted
declined
scheduled

Notes:

  • Toast requests are voluntary.
  • Admin approval is required before scheduling.

guest_messages

Stores optional advice, wishes, or memories.

id uuid primary key
invitation_id uuid not null references invitations(id) on delete cascade
type text not null
message text not null
created_at timestamptz not null

Recommended message types:

advice
memory
wish

admin_audit_events optional

Useful but not required for the first pass.

id uuid primary key
admin_email text not null
action text not null
target_type text
target_id uuid
metadata jsonb
created_at timestamptz not null

Derived RSVP status logic

Admin-facing status should be computed instead of stored redundantly.

Suggested derived statuses:

Pending
  invitation_response.submitted_at is null

Partially answered
  submitted_at exists and at least one member has attending = null

Attending
  at least one member has attending = true and no members are pending

Declined
  all members have attending = false and no members are pending

Partial
  at least one member attending = true and at least one member attending = false

Dashboard metrics can derive:

  • total invitations
  • total invited members
  • attending members
  • declined members
  • pending members
  • invitations with submitted responses
  • invitations with uploaded photos
  • song request count
  • pending toast requests

Guest-facing invite flow

Route:

/invite/[token]

Flow:

  1. Resolve invitation by token.
  2. If token is invalid, show a friendly invalid-invitation page.
  3. If valid, render personalized wedding page shell.
  4. Load guest-facing copy based on invitations.language.
  5. Show static wedding information:
    • hero
    • date
    • countdown
    • location
    • accommodation information
    • short couple story placeholder
  6. Show RSVP section with all invitation members.
  7. For each member:
    • attending yes/no
    • meal preference if attending
    • dietary restrictions if attending
  8. Show group logistics:
    • arrival date
    • departure date
    • stay location
    • coming from
    • transport help
    • message to the couple
  9. Show optional interaction sections:
    • song requests
    • toast/speech request
    • advice/wish/memory
    • one photo upload
  10. Before deadline, allow edits.
  11. After deadline, show submitted answers read-only and explain that changes require contacting the couple.

Guest-facing localized content should include at least:

  • page headings
  • navigation/section labels
  • RSVP form labels
  • validation messages
  • success/error messages
  • deadline/read-only messages
  • upload instructions
  • optional interaction prompts

Wedding factual content, such as venue names and addresses, can remain shared, but surrounding explanatory copy should be localized.

Admin portal plan

Admin routes:

/admin
/admin/guests
/admin/guests/new
/admin/guests/[id]

Although the domain model is invitations, the UI can still use guest-friendly labels like Guests.

/admin

Dashboard cards:

  • Total invitations.
  • Total invited people.
  • RSVP pending.
  • Attending people.
  • Declined people.
  • Uploaded photos.
  • Song requests.
  • Pending toast requests.

/admin/guests

Invitation list table:

  • display name
  • language
  • contact email
  • contact phone
  • members count
  • derived RSVP status
  • attending count
  • invite link
  • last updated

Actions:

  • copy invite link
  • view details
  • edit

/admin/guests/new

Create invitation form:

  • display name
  • language
  • contact email
  • contact phone
  • max party size
  • allow member edits
  • notes
  • initial members

On submit:

  • generate token
  • create invitation
  • create initial members
  • show generated invite URL

The language field is required.

/admin/guests/[id]

Detail page:

  • invitation metadata
  • invite token/link
  • member list and per-person RSVP answers
  • group logistics
  • uploaded photo preview/link
  • song requests
  • toast requests with admin status controls
  • advice/wish/memory messages
  • admin notes

Admin actions:

  • edit invitation
  • add/edit/remove members if needed
  • regenerate invite token
  • manually adjust RSVP/member data
  • accept/decline/schedule toast request

Authentication plan

Use Google OAuth with an email allowlist.

Implementation targets:

auth.ts
app/api/auth/[...nextauth]/route.ts
middleware.ts

Rules:

  • Only emails listed in ADMIN_EMAILS may access /admin and /admin/*.
  • Non-admin Google users are rejected or shown a forbidden page.
  • Guest invite pages do not require login.
  • Admin emails remain in .env for the bootstrap.

Before implementation, verify the exact current Next.js/Auth.js setup for the installed framework version.

MinIO upload plan

Use AWS SDK S3-compatible client.

Upload flow:

  1. Guest selects one image.
  2. Client asks the app for a presigned upload URL.
  3. App validates invitation token and deadline.
  4. App creates an object key:
invitations/{invitationId}/{uuid}-{safeFilename}
  1. Browser uploads directly to MinIO using presigned PUT.
  2. Client calls complete endpoint.
  3. App upserts photo_uploads metadata for the invitation.

Validation:

  • Allow only image MIME types:
    • image/jpeg
    • image/png
    • image/webp
  • Enforce a max size.
  • Enforce one current photo per invitation.

Admin viewing:

  • Generate short-lived signed GET URLs for previews/downloads.

Docker Compose plan

Services:

app
postgres
minio
minio-init

Postgres:

  • expose 5432
  • database: wedding
  • user: wedding
  • password: wedding

MinIO:

  • API: 9000
  • console: 9001
  • bucket: wedding-uploads

minio-init:

  • wait for MinIO
  • create bucket if it does not exist

Implementation sequence

Step 1: Review framework-specific docs

Because this project uses Next.js 16, review the locally installed Next documentation before coding framework-specific pieces.

Focus areas:

  • App Router route handlers.
  • Server actions/forms.
  • Middleware behavior.
  • Authentication integration compatibility.

Step 2: Add dependencies

Install likely packages:

npm install kysely pg zod next-auth @aws-sdk/client-s3 @aws-sdk/s3-request-presigner nanoid
npm install -D @types/pg kysely-codegen

Adjust if current package versions require different auth package names or setup.

Step 3: Add environment handling

  • Create .env.example.
  • Add typed config helper in lib/config.ts.
  • Validate required env vars at startup/server usage.

Step 4: Add Docker Compose

  • Add docker-compose.yml.
  • Define Postgres service.
  • Define MinIO service.
  • Define MinIO bucket initialization.
  • Optionally define app service.

Step 5: Add Kysely database client

  • Create lib/db/client.ts.
  • Create initial manual DB types in lib/db/types.ts.
  • Add SQL migration runner in scripts/migrate-sql.mjs.
  • Add package script:
"db:migrate": "node scripts/migrate-sql.mjs"

Step 6: Create initial SQL migration

Create migrations/0001_initial_schema.sql for:

  • invitations
  • invitation_members
  • invitation_responses
  • photo_uploads
  • song_requests
  • toast_requests
  • guest_messages
  • optional admin_audit_events

Step 7: Add query modules

Create query helpers for:

  • invitations
  • invitation members
  • invitation responses
  • uploads
  • song requests
  • toast requests
  • guest messages

Keep SQL explicit and typed through Kysely.

Step 8: Add validation schemas

Use Zod for:

  • creating/updating invitations
  • creating/updating members
  • submitting RSVP/member attendance
  • submitting logistics
  • song requests
  • toast requests
  • guest messages
  • upload presign/complete requests

Invitation validation must require a supported language code.

Step 9: Implement Google admin auth

  • Configure Google provider.
  • Restrict sign-in to ADMIN_EMAILS.
  • Protect /admin routes.
  • Add sign-in/sign-out UI as needed.

Step 10: Build admin MVP

Implement:

  • /admin dashboard.
  • /admin/guests invitation list.
  • /admin/guests/new create form.
  • /admin/guests/[id] detail page.

Minimum admin operations:

  • create invitation
  • add members
  • copy invite link
  • view submitted RSVP data
  • view optional guest interactions

Step 11: Build guest invite MVP

Implement /invite/[token] with:

  • invalid token handling
  • personalized greeting
  • language-aware guest-facing copy based on invitations.language
  • wedding info shell
  • countdown
  • member attendance form
  • per-member meal/dietary fields
  • group logistics form
  • submission/edit behavior until deadline

Step 12: Add optional guest interactions

Add form sections for:

  • up to 3 song requests
  • optional toast/speech request
  • advice/wish/memory message

Admin should be able to view all of them.

Step 13: Add MinIO photo upload

Implement:

  • S3 client.
  • presigned PUT endpoint.
  • upload complete endpoint.
  • one-photo-per-invitation metadata upsert.
  • admin signed preview/download URL.

Step 14: Add basic design shell

Replace default starter UI with a simple wedding-appropriate layout:

  • elegant typography
  • hero section
  • countdown
  • location/accommodation placeholders
  • story placeholder
  • clear guest forms
  • responsive mobile-first layout

Final visual design can remain future work.

Step 14.5: Add localization foundation

Add a simple localization layer for guest-facing invitation pages.

Recommended bootstrap approach:

  • Keep supported language codes in config.
  • Store the selected language on each invitation.
  • Resolve guest-facing copy from invitations.language on /invite/[token].
  • Create translation dictionaries for guest-facing copy.
  • Fallback to a default language only if necessary, but admin should always select a supported language.

Suggested structure:

lib/
  i18n/
    config.ts
    dictionaries/
      en.ts
      es.ts
      de.ts

Do not rely only on browser language detection for invited guests. The invitation language is the source of truth.

Step 15: Verification

Run:

npm run lint
npm run build

Manual happy path:

  1. Start Docker Compose.
  2. Run migrations.
  3. Log in as admin with allowed Google account.
  4. Create an invitation with multiple members.
  5. Set the invitation language.
  6. Copy invite link.
  7. Open invite link.
  8. Confirm the page renders in the invitation's configured language.
  9. Submit attendance with different meal preferences per member.
  10. Add travel details.
  11. Add song request, toast request, and message.
  12. Upload one photo.
  13. Return to admin and confirm all data is visible.
  14. Test invalid invite token.
  15. Test non-admin access to /admin.
  16. Test deadline read-only behavior.

Open implementation questions

These can be decided during implementation:

  1. Should guests be able to add/edit member names when allow_member_edits = true, or only fill unnamed placeholders?
  2. What should the exact RSVP deadline be?
  3. What max upload size should be allowed?
  4. Should admin be able to export guest/RSVP data as CSV in the first version?
  5. Which exact languages should be supported in the first version?

The first useful milestone should be:

  • Docker Compose with Postgres and MinIO.
  • Initial pure SQL migration executed by scripts/migrate-sql.mjs.
  • Google admin login restricted by .env emails.
  • Admin can create invitation with members.
  • Admin can select a language per invitation.
  • Guest can open /invite/[token] and submit per-member RSVP answers.
  • Guest invite page renders in the invitation's configured language.
  • Admin can see submitted answers.

After that, add photo uploads and the fun interaction sections.