Skip to main content

System Overview

Reservations is built as a monolithic application that compiles into a single binary. The architecture combines a Go backend with two embedded React frontends, all served through a unified HTTP server.

Tech Stack

Backend Technologies

Go 1.21+

HTTP Server & Business Logic
  • Chi router for HTTP routing
  • Standard library HTTP server
  • Context-based request handling
  • Clean architecture patterns

PostgreSQL + PostGIS

Data Persistence
  • PostgreSQL 14+ for relational data
  • PostGIS extension for geospatial features
  • pgx/v5 driver for performance
  • Connection pooling with pgxpool

JWT Authentication

Security & Auth
  • Access tokens (15 min expiry)
  • Refresh tokens (7 day expiry)
  • Token versioning for revocation
  • OAuth2 integration

Resend

Email Delivery
  • Transactional emails
  • Scheduled email reminders
  • HTML templates with i18n
  • Email cancellation support

Frontend Technologies

React 19

UI Framework
  • Functional components with hooks
  • React Router for navigation
  • TanStack Query for data fetching
  • React Compiler for optimization

Vite

Build Tool
  • Fast HMR in development
  • Optimized production builds
  • Code splitting
  • Asset optimization

Tailwind CSS 4

Styling
  • Utility-first CSS
  • JIT compilation
  • Custom design system
  • Responsive design

FullCalendar

Scheduling UI
  • Interactive calendar views
  • Drag-and-drop booking
  • Multiple view modes
  • Event management

Application Structure

Backend Architecture

The backend follows clean architecture principles with clear separation of concerns:
backend/
├── cmd/
│   ├── main.go              # Application entry point
│   └── config/
│       └── config.go        # Environment configuration
├── internal/
│   ├── api/                 # HTTP layer
│   │   ├── router.go        # Route definitions
│   │   ├── handler/         # HTTP handlers
│   │   └── middleware/      # Auth, RBAC, logging
│   ├── service/             # Business logic
│   │   ├── auth/
│   │   ├── booking/
│   │   ├── merchant/
│   │   └── email/
│   ├── domain/              # Domain models & interfaces
│   ├── repository/          # Data access implementation
│   │   └── db/
│   └── types/               # Shared types & enums
├── pkg/                     # Reusable packages
│   ├── db/                  # Database utilities
│   ├── httputil/
│   ├── oauthutil/
│   └── validate/
└── emails/                  # Email templates

Key Components

The router handles request routing and serves both API and frontend apps.From backend/internal/api/router.go:45-113:
func NewRouter(h *Handlers) *chi.Mux {
    r := chi.NewRouter()
    
    r.Use(chiMiddleware.Logger)
    r.Use(chiMiddleware.AllowContentType("application/json"))
    
    // API routes
    r.Route("/api/v1", func(r chi.Router) {
        r.Mount("/auth", h.Auth.Routes())
        r.Mount("/bookings", h.Bookings.Routes())
        r.Mount("/merchants/{merchantName}", h.Merchants.Routes())
        
        r.Route("/merchant", func(r chi.Router) {
            r.Use(h.Middleware.Authentication)
            r.Use(h.Middleware.Language)
            // ... merchant routes with RBAC
        })
    })
    
    // Frontend routing (NotFound handler)
    r.NotFound(func(w http.ResponseWriter, r *http.Request) {
        if strings.HasPrefix(r.Host, "app.") {
            jabulani.ServeHTTP(w, r)
        } else {
            tango.ServeHTTP(w, r)
        }
    })
    
    return r
}
The router uses the NotFound handler to serve frontend apps, allowing the Go server to handle both API and SPA routing.

Frontend Architecture

frontend/
├── apps/
│   ├── jabulani/            # Merchant application
│   │   ├── src/
│   │   │   ├── routes/      # Page components
│   │   │   ├── components/  # UI components
│   │   │   ├── api/         # API client
│   │   │   └── main.jsx     # Entry point
│   │   ├── vite.config.js
│   │   └── package.json
│   └── tango/               # Customer application
│       └── (similar structure)
└── packages/                # Shared code
    ├── components/          # Shared UI components
    ├── lib/                 # Utilities & hooks
    └── assets/              # Images, fonts

Frontend Features

Jabulani (Merchant App)

Key Features:
  • Full calendar view with FullCalendar
  • Drag-and-drop booking management
  • Service catalog CRUD
  • Customer management with blacklist
  • Team member management
  • Business hours configuration
  • External calendar integration
  • Analytics dashboard with Recharts

Tango (Customer App)

Key Features:
  • Merchant discovery
  • Service browsing
  • Self-service booking flow
  • Booking cancellation
  • Account management
  • Multi-language support
  • Responsive mobile design

Data Flow

Booking Creation Flow

Here’s how a customer booking flows through the system:
1

Customer selects service and time

Customer browses merchant’s services in Tango app and selects a time slot.
// Frontend makes API request
POST /api/v1/bookings/customer
{
  "merchantName": "example-salon",
  "serviceId": 123,
  "locationId": 1,
  "timeStamp": "2026-03-10T14:00:00Z",
  "customerNote": "First time customer"
}
2

JWT middleware authenticates request

The middleware validates the JWT token and extracts user information.From backend/internal/api/middleware/jwt/jwt.go:
func (m *Manager) Authentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := extractTokenFromHeader(r)
        claims, err := ValidateAccessToken(token)
        if err != nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        ctx := context.WithValue(r.Context(), userIDKey, claims.UserID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
3

Handler validates and delegates to service

The booking handler validates input and calls the service layer.
func (h *Handler) CreateByCustomer(w http.ResponseWriter, r *http.Request) {
    var input booking.CreateByCustomerInput
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    err := h.service.CreateByCustomer(r.Context(), input)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    w.WriteHeader(http.StatusCreated)
}
4

Service validates business rules

The booking service checks:
  • Merchant exists and is active
  • Service is available
  • Time slot is valid (within booking window)
  • Customer is not blacklisted
  • No scheduling conflicts
5

Transaction creates booking records

All database operations happen in a transaction:
err := s.txManager.WithTransaction(ctx, func(tx db.DBTX) error {
    // 1. Create booking
    bookingId, _ := s.bookingRepo.WithTx(tx).NewBooking(ctx, booking)
    
    // 2. Create booking phases (service sub-tasks)
    _ = s.bookingRepo.WithTx(tx).NewBookingPhases(ctx, phases)
    
    // 3. Create booking details (pricing)
    _ = s.bookingRepo.WithTx(tx).NewBookingDetails(ctx, details)
    
    // 4. Create participant records
    _ = s.bookingRepo.WithTx(tx).NewBookingParticipants(
        ctx, participants,
    )
    
    return nil // Commits transaction
})
6

Send confirmation email

After successful booking, send confirmation email via Resend:
emailData := email.BookingConfirmationData{
    Time:        "14:00 - 15:00",
    Date:        "Tuesday, March 10",
    Location:    "123 Main St, City",
    ServiceName: "Haircut",
    TimeZone:    "America/New_York",
    ModifyLink:  "http://example.com/m/salon/cancel/456",
}

err = s.mailer.BookingConfirmation(
    ctx, language.English, customer.Email, emailData,
)
7

Schedule reminder email

If booking is >24 hours away, schedule reminder email:
if hoursUntilBooking >= 24 {
    reminderDate := bookingTime.Add(-24 * time.Hour)
    emailId, _ := s.mailer.BookingReminder(
        ctx, lang, customer.Email, emailData, reminderDate,
    )
    
    // Store email ID for cancellation
    _ = s.bookingRepo.UpdateEmailIdForBooking(
        ctx, bookingId, emailId, customerId,
    )
}

Authentication & Authorization

JWT Token Flow

// Short-lived (15 minutes)
type AccessTokenClaims struct {
    UserID      uuid.UUID
    MerchantID  *int
    LocationID  *int
    EmployeeID  *int
    Role        *types.EmployeeRole
    jwt.RegisteredClaims
}

func NewAccessToken(
    userId uuid.UUID,
    merchantId, locationId, employeeId *int,
    role *types.EmployeeRole,
) (string, error) {
    claims := AccessTokenClaims{
        UserID:     userId,
        MerchantID: merchantId,
        LocationID: locationId,
        EmployeeID: employeeId,
        Role:       role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(
                time.Now().Add(15 * time.Minute),
            ),
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(jwtAccessSecret))
}

Role-Based Access Control

The system implements RBAC with three merchant roles:
Full access to all merchant features:
  • Delete merchant account
  • Update merchant name
  • Manage team members and roles
  • All staff and admin permissions
RBAC middleware from backend/internal/api/middleware/rbac.go:
func (m *Manager) RoleBasedAccessControl(
    allowedRoles ...types.EmployeeRole,
) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            employee := jwt.MustGetEmployeeFromContext(r.Context())
            
            for _, role := range allowedRoles {
                if employee.Role == role {
                    next.ServeHTTP(w, r)
                    return
                }
            }
            
            http.Error(w, "Forbidden", http.StatusForbidden)
        })
    }
}
Usage in router:
r.Group(func(r chi.Router) {
    r.Use(h.Middleware.RoleBasedAccessControl(
        types.EmployeeRoleOwner,
    ))
    
    r.Delete("/", h.Merchant.Delete)
    r.Patch("/name", h.Merchant.UpdateName)
})

OAuth Integration

Support for Google and Facebook OAuth:
func (s *Service) GoogleCallback(
    ctx context.Context, 
    code string,
) (jwt.TokenPair, error) {
    // Exchange code for token
    token, err := googleConf.Exchange(ctx, code)
    if err != nil {
        return jwt.TokenPair{}, err
    }
    
    // Get user info
    client := googleConf.Client(ctx, token)
    resp, _ := client.Get(
        "https://openidconnect.googleapis.com/v1/userinfo",
    )
    
    var googleUser struct {
        Id         string `json:"sub"`
        GivenName  string `json:"given_name"`
        FamilyName string `json:"family_name"`
        Email      string `json:"email"`
    }
    json.NewDecoder(resp.Body).Decode(&googleUser)
    
    // Find or create user
    userId, err := s.userRepo.FindOauthUser(
        ctx, types.AuthProviderTypeGoogle, googleUser.Id,
    )
    if errors.Is(err, pgx.ErrNoRows) {
        // Create new user
        userId, _ = uuid.NewV7()
        s.userRepo.NewUser(ctx, domain.User{
            Id:           userId,
            FirstName:    googleUser.GivenName,
            LastName:     googleUser.FamilyName,
            Email:        googleUser.Email,
            AuthProvider: &types.AuthProviderTypeGoogle,
            ProviderId:   &googleUser.Id,
        })
    }
    
    // Generate JWT tokens
    accessToken, _ := jwt.NewAccessToken(userId, nil, nil, nil, nil)
    refreshToken, _ := jwt.NewRefreshToken(
        userId, nil, nil, nil, nil, 0,
    )
    
    return jwt.TokenPair{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
    }, nil
}

Database Schema

Core Entities

The database schema supports multi-tenancy and complex booking relationships:
-- Merchants (multi-tenant isolation)
merchants
  - id
  - name
  - url_name (unique)
  - timezone
  - currency
  - settings (JSONB)
  - created_at

-- Users (customers and employees)
users
  - id (UUID)
  - first_name
  - last_name
  - email (unique)
  - phone_number
  - password_hash
  - jwt_refresh_version
  - auth_provider (google/facebook/null)
  - provider_id
  - preferred_lang

-- Employees (users who work for merchants)
employees
  - id
  - merchant_id
  - user_id
  - location_id
  - role (owner/admin/staff)
  - created_at

Email System

The email system uses Resend for delivery and supports:

Immediate Emails

Sent immediately after events:
  • Booking confirmation
  • Booking cancellation
  • Booking modification
  • Password reset
  • Email verification

Scheduled Emails

Scheduled for future delivery:
  • Booking reminders (24h before)
  • Cancellable via email ID
  • Reschedulable when booking changes

Email Templates

Templates use Go’s html/template with i18n support:
type Service struct {
    templates *template.Template
    bundle    *i18n.Bundle
    client    *resend.Client
}

func (s *Service) executeTemplate(
    name string,
    lang language.Tag,
    data any,
) string {
    var buf bytes.Buffer
    
    tmpl := s.templates.Lookup(name + ".html")
    dataMap := utils.StructToMap(data)
    dataMap["Lang"] = lang.String()
    
    err := tmpl.Execute(&buf, dataMap)
    return buf.String()
}
Template with i18n:
<!-- BookingConfirmation.html -->
<h1>{{T .Lang "booking_confirmation.title"}}</h1>
<p>{{T .Lang "booking_confirmation.message" .}}</p>
<p><strong>{{T .Lang "booking_confirmation.date"}}:</strong> {{.Date}}</p>
<p><strong>{{T .Lang "booking_confirmation.time"}}:</strong> {{.Time}}</p>
Translations in TOML:
# emails.en.toml
[booking_confirmation]
title = "Booking Confirmed"
message = "Your appointment has been confirmed."
date = "Date"
time = "Time"

# emails.hu.toml
[booking_confirmation]
title = "Foglalás megerősítve"
message = "Az időpontja megerősítésre került."
date = "Dátum"
time = "Időpont"

Deployment

Single Binary Deployment

The application compiles into a single, self-contained binary:
// frontend/apps/jabulani/embed.go
//go:build prod
package jabulani

import "embed"

//go:embed dist
var dist embed.FS

//go:embed dist/assets
var assets embed.FS
This embeds the compiled React apps into the Go binary, enabling:
  • No separate frontend deployment
  • Single container/process
  • Simplified CI/CD
  • Atomic deployments

Environment Configuration

All configuration via environment variables:
type Config struct {
    PORT    string
    APP_ENV string
    
    DB_HOST     string
    DB_PORT     string
    DB_DATABASE string
    DB_USERNAME string
    DB_PASSWORD string
    
    JWT_ACCESS_SECRET   string
    JWT_ACCESS_EXP_MIN  int
    JWT_REFRESH_SECRET  string
    JWT_REFRESH_EXP_MIN int
    
    RESEND_API_TEST string
    ENABLE_EMAILS   bool
    
    GOOGLE_OAUTH_CLIENT_ID     string
    GOOGLE_OAUTH_CLIENT_SECRET string
    // ...
}

Production Build

make build
Creates:
  • backend/bin/reservations (Linux/macOS)
  • backend/bin/reservations.exe (Windows)
With embedded:
  • Jabulani frontend
  • Tango frontend
  • Email templates

Performance Considerations

Database Optimization

  • Connection pooling with pgxpool
  • Prepared statements
  • Efficient queries with joins
  • Indexes on foreign keys
  • PostGIS for geospatial queries

API Performance

  • Chi router (low overhead)
  • Context-based request handling
  • Streaming responses where applicable
  • Gzip compression
  • Static asset caching

Frontend Optimization

  • Code splitting with Vite
  • React Compiler for optimizations
  • TanStack Query caching
  • Lazy loading routes
  • Optimized images and assets

Scalability

  • Stateless API (horizontal scaling)
  • Database connection pooling
  • Transaction isolation
  • Efficient email queuing
  • Timezone-aware scheduling

Next Steps

API Reference

Explore the REST API endpoints and request/response formats

Deployment Guide

Learn how to deploy the application to production

Build docs developers (and LLMs) love