Overview
Amigos Unite is a full-stack web application designed to coordinate community events through a structured API-driven architecture. The system supports user authentication, event management, geospatial location handling, and role-based participation workflows.
The platform is implemented as a Rails 7 API backend paired with a React/TypeScript frontend application. It was designed to demonstrate clean domain modeling, API-first architecture, and secure authentication boundaries between client and server applications.
The system includes structured models for events, participants, event locations, and user profiles, allowing event organizers to coordinate activities through clearly defined application workflows.
Links
GitHub Repositories
Backend API
https://github.com/sagacic-tim/Amigos_Unite_Api
Frontend Application
https://github.com/sagacic-tim/Amigos_Unite_App
Architecture
The system is designed as a two-tier API architecture:
Frontend Application
React + TypeScript client communicating through Axios service layers.
Backend API
Rails 7 API-only application exposing structured REST endpoints.
Core domain models include:
- Amigos
- Amigos Details
- Amigos Confirmations
- Amigos Locations/Places
- Amigos Locations Connectors
- Events
- Events Management/Coordination
- Events Locations/Places
- Events Locations Connectors
- Events Photos
- Events Participants
- Messaging
Authentication is implemented using Devise with JWT tokens, enabling stateless authorization between the React client and Rails backend.
Location data is modeled using PostgreSQL with PostGIS, allowing geospatial queries for event locations.

Amigos Unite — End-to-End Software Flow and Technology Stack

1) Client Application Layer (Browser SPA)
React + TypeScript (Vite)
- The frontend is a modern SPA that supports:
- Public browsing (e.g., listing scheduled events)
- Authenticated management (create/manage events, manage participants and coordinators)
- Routing and UI state are authentication-aware:
- Navigation and pages are gated by login state (e.g., “Profile”, “Manage My Events”, “Create New Event” appear only when authenticated)
Axios Service Layer
- Two Axios clients are used to separate concerns cleanly:
- publicApi
- Uses withCredentials: true so cookies can flow cross-origin (CSRF + JWT cookies)
- Automatically attaches X-CSRF-Token on mutating requests (POST/PUT/PATCH/DELETE)
- privateApi
- Same cookie behavior, plus centralized 401 handling
- Supports an optional refresh-and-retry workflow (where implemented)
- publicApi
Typed Domain Services (example: EventService)
- The SPA communicates through typed service methods rather than raw HTTP calls.
- Responses are normalized from JSON:API (data/attributes/relationships/included) into strongly typed frontend models such as Event and EventLocation.
- The service exposes high-level operations including:
- fetchEvents() (public)
- fetchMyEvents() (auth-only)
- createEvent() (auth + CSRF)
- updateEvent() / deleteEvent() (auth + CSRF)
Client-Side Security Contract
- CSRF is enforced for browser safety, including:
- “Public browsing” GET requests mint CSRF cookies so later state-changing requests can be performed safely from the SPA.
- JWT is required for protected operations, including:
- Event creation, updates, and deletes
- “My Events” management endpoints
- Role management endpoints
2) API Gateway & Request Processing (Rails API)
Ruby on Rails 7.1 (API-only)
- API endpoints are organized under /api/v1/*.
- Response contracts are consistent across resources:
- Public endpoints return stable JSON payloads
- Protected endpoints enforce authentication + authorization policies
- Mutations enforce CSRF validation
Authentication & Authorization
- Devise provides the session lifecycle foundation for the API.
- JWT Authentication
- JWTs are issued at login and stored as secure cookies (cookies.signed[:jwt])
- Requests can authenticate via cookie and/or bearer token depending on endpoint
- CSRF Protection
- A CSRF cookie is minted on API GETs for SPA bootstrapping
- Mutating requests must provide X-CSRF-Token matching the CSRF-TOKEN cookie
Policy Layer
- Authorization is centralized in EventPolicy, which governs actions such as:
- create? requires a logged-in user
- update? allowed for lead/assistant/admin
- destroy? allowed for lead/admin only
- manage_roles? allowed for lead/admin only (explicitly excludes assistant)
- manage_connectors? allowed for lead/assistant/admin
3) Domain Logic Layer (Service Objects + Invariants)
Complex workflows are implemented as service objects to preserve invariants and keep controllers thin:
- Events::CreateEvent
- Creates the Event record
- Creates the lead-coordinator connector
- Normalizes speakers/performers inputs
- Optionally triggers location creation/upsert when location attributes are provided
- Events::UpsertPrimaryLocation
- Enforces exactly one primary location per event (DB constraints + service-level invariants)
- Creates or updates EventLocation
- Creates or updates the primary EventLocationConnector
- Integrates with the location image persistence workflow
- Events::TransferLead
- Performs an atomic lead coordinator reassignment
- Updates connectors (demote old lead, promote new lead)
- Updates event.lead_coordinator_id in the same transaction
- Events::ChangeRole
- Changes roles between participant and assistant coordinator
- Explicitly rejects lead promotion (which must flow through the dedicated transfer-lead service)
4) Location Intelligence Layer (Google Places + Photos + Persistence)
Google Places integration is used as a real-world location intelligence layer, not merely as a convenience lookup.
Google APIs
- Google Places API
- Validates venues using real-world records (place_id)
- Retrieves canonical venue data (name, phone, structured address components where available)
- Retrieves photo references representing Google Maps user-uploaded photos for a place
- Google Places Photos endpoint
- Fetches images by photo_reference
- The system selects a “best” image using rule-based heuristics (e.g., highest resolution / availability)
- The selected image is persisted locally
Photo Selection + Persistence Pipeline
- Fetch candidate photos for a place_id
- Select one via deterministic heuristics
- Download server-side
- Persist locally via Active Storage
- Media is controlled by the application (not hot-linked)
- Attribution required by Google Places is persisted (e.g., location_image_attribution)
- The workflow is compatible with background execution to avoid blocking API requests:
- Image processing and location image processing jobs support asynchronous handling
Data Model Invariants
- EventLocation stores structured address fields and place_id
- EventLocationConnector enforces:
- Uniqueness of (event_id, event_location_id)
- Uniqueness of the primary location per event (is_primary = true unique index)
- Image metadata and attribution are stored alongside the location record and/or attachment metadata.
5) Data Layer
PostgreSQL 17
- Relational store for core domain entities:
- Amigos, Events, connectors, locations, JWT denylist, login activities
- Data integrity is enforced at multiple levels:
- Rails validations (e.g., uniqueness constraints, invariants)
- Database indexes and constraints (e.g., primary-location uniqueness, connector uniqueness)
PostGIS (optional / upgrade path)
- PostGIS is not required unless geospatial types/extensions are actively used.
- The design supports a future transition to true geospatial indexing/queries if location features expand.
6) Background Processing & Media Pipeline
Active Job + Background Workers
- Background-friendly architecture supports Sidekiq-compatible workloads such as:
- Remote avatar fetch + processing
- Location image processing (Google photo selection/download/attach)
- Image normalization/resizing via image_processing + vips
Redis
- Primary backing store for Sidekiq job queues
- Also supports optional caching/rate limiting depending on production configuration
7) Containerization & Local Orchestration
Docker + Docker Compose
- Containerized system components include:
- Rails API container
- PostgreSQL container
- Redis container (for Sidekiq)
- Sidekiq worker container (recommended separation from web)
- The architecture maintains clear separation of responsibilities:
- Web process (Rails/Puma)
- Worker process (Sidekiq)
- Persistent volumes (DB data, logs, uploads as required)
8) CI/CD Pipeline (GitHub Actions → GHCR → VPS)
GitHub Actions
- PR validation (backend-ci.yml)
- Runs RSpec
- Validates schema load and core invariants in CI
- Main branch pipeline (ghcr.yml)
- Runs the test suite again as an authoritative gate
- Builds a production Docker image
- Pushes the image to GitHub Container Registry (GHCR) tagged as:
- main
- sha-<short>
GitHub Container Registry (GHCR)
- Stores immutable deployable Docker images
- The production VPS pulls images directly from GHCR
VPS Deployment
- VPS runtime environment includes Docker + Docker Compose
- Deployment flow:
- Merge to main triggers GHCR build + push
- VPS pulls the latest image tag (e.g., ghcr.io/sagacic-tim/amigos_unite_api:main)
- VPS runs docker compose up -d to recreate containers with the updated image
- Nginx (or similar) terminates TLS and reverse-proxies to the Rails container
Screenshots
Event creation form


User profile page

![]()