Introduction
I recently built a dating web application focused on coffee enthusiasts using a server-side rendered approach with Go and modern frontend techniques. In this article, I'll share the technical architecture, challenges, and interesting solutions implemented in this project.
Technology Stack
Instead of following the typical SPA route with React or Vue, I chose a server-side approach with sprinkles of interactivity:
- Backend: Go (net/http) for server logic and routing
- Frontend: HTMX and AlpineJS for interactive elements without a heavy JavaScript framework
- Database: PostgreSQL with GORM for ORM functionality
- Search: Typesense for fast, vector-based recommendations
- Media Storage: MinIO (S3-compatible) for photo storage
- ML Features: Face recognition for profile verification
Why This Stack?
I deliberately chose this stack for several reasons:
- Performance: Server-rendered HTML is fast and lightweight
- Simplicity: Avoiding JavaScript frameworks reduces complexity
- SEO-friendly: Content is fully rendered at request time
- Progressive Enhancement: The site works even without JavaScript
- Development Speed: Reduced context-switching between languages/paradigms
Key Technical Implementations
Server Architecture in Go
The application uses Go's standard net/http
package rather than a framework. This gave me full control over the request lifecycle with minimal dependencies:
func main() {
// Initialize database connection db := database.InitDB()
// Create route handlers mux := http.NewServeMux()
// Register routes mux.HandleFunc("/", handlers.HomeHandler)
mux.HandleFunc("/profile", handlers.ProfileHandler)
// ...more routes
// Start server log.Println("Starting server on :8080")
http.ListenAndServe(":8080", middlewares.LogRequest(mux))
}
This minimalist approach avoids the overhead of complex routing frameworks while maintaining readability.
HTMX for Seamless Interactions
Instead of relying on a heavy client-side framework, I used HTMX to enable dynamic content updates without full page reloads:
<button hx-post="/like/{{.User.ID}}"
hx-swap="outerHTML"
hx-target="#like-button-{{.User.ID}}"> Like Profile
</button>
This approach allows for dynamic interactions while keeping most logic on the server.
AlpineJS for Client-Side State
For interactive components requiring client-side state, I used AlpineJS:
<div x-data="{ isUploading: false, fileName: '' }"> <input type="file"
@change="fileName = $event.target.files[0].name; isUploading = true">
<button x-show="isUploading"
type="submit"> Upload Photo
</button> </div>
Alpine provides just enough reactivity without the overhead of a full framework.
Profile Verification with Face Detection
I implemented a secure profile verification system using face detection:
- Users take a selfie in real-time
- The server analyzes the photo using a face recognition library
- The system confirms the person in profile photos matches the verification photo
func VerifyFace(w http.ResponseWriter, r *http.Request) {
// Get the user's verification photo file, _, err := r.FormFile("verification-photo")
// Process image with face recognition faces, err := recognition.DetectFaces(file)
// Verify against existing profile photos isMatch, err := recognition.VerifyFaceMatch(userID, faces)
// Update verification status if isMatch {
db.Model(&user).Update("IsPhotoVerified", true)
}
// Respond with result // ... }
Vector-Based Search with Typesense
For efficient matching and recommendations, I implemented vector-based search using Typesense:
- User profiles are converted to vector representations
- Interests, personality traits, and demographics are encoded
- Typesense enables similarity searches much faster than SQL could provide
func SearchUsers(params SearchParams) (*api.SearchResult, error) {
searchParams := &api.SearchCollectionParams{
Q: params.Query,
QueryBy: "first_name, last_name, bio, interests",
FilterBy: params.FilterBy,
SortBy: params.SortBy,
// Additional search parameters }
return TypesenseClient.Collection("users").Documents().Search(searchParams)
}
S3-Compatible Media Storage with MinIO
For scalable photo storage, I integrated MinIO:
func UploadPhoto(userID uuid.UUID, file io.Reader) (string, error) {
bucketName := "profile-photos" objectName := fmt.Sprintf("%s/%s.jpg", userID, uuid.New())
// Upload to MinIO _, err := minioClient.PutObject(
context.Background(),
bucketName,
objectName,
file,
-1,
minio.PutObjectOptions{ContentType: "image/jpeg"},
)
return objectName, err
}
Performance Considerations
Database Optimization
To keep the application responsive, I implemented several database optimizations:
- Strategic indexes on frequently queried fields
- Offloaded search to Typesense to reduce database load
- Used query batching to minimize database roundtrips
Computational Optimization
For computationally expensive operations like vector calculations:
- Cached vector representations when profiles don't change
- Moved vector similarity calculations to Typesense
- Implemented background processing for non-time-critical operations
Challenges Encountered
Face Recognition Accuracy
Getting face recognition to work reliably across diverse users was challenging. I addressed this by:
- Implementing clear photo guidelines for users
- Adding a specific verification pose requirement
- Balancing security needs with user experience
Typesense Integration
Integrating Typesense required careful handling of:
- Synchronizing database and search index
- Converting user data to appropriate vector representations
- Handling graceful fallbacks when search services experience issues
Conclusion
Building this application with Go, HTMX, and AlpineJS proved to be efficient and resulted in a fast, lightweight experience. The server-side rendering approach combined with targeted client-side interactivity delivered both performance and a great user experience.
This architecture won't be right for every project, but for applications where content delivery and performance are priorities, this stack offers compelling advantages over heavier client-side frameworks.
If you're interested in exploring the app or have technical questions, I'd love to hear your feedback!