Skip to content

Latest commit

 

History

History
1484 lines (1218 loc) · 36.1 KB

File metadata and controls

1484 lines (1218 loc) · 36.1 KB

Publishing & Frontend Integration Guide

Project: Hexagon Feed System (HexFeed)
Last Updated: October 19, 2025
Status: Production-Ready Backend


📋 Table of Contents

  1. Publishing Options
  2. Docker Hub Publishing
  3. Cloud Deployment
  4. API Documentation Export
  5. Frontend Integration
  6. Testing with Frontend
  7. CORS Configuration
  8. WebSocket Integration
  9. Example Frontend Code

1. Publishing Options

Option A: Docker Hub (Recommended for Quick Testing)

  • ✅ Easy to deploy and share
  • ✅ Works with any cloud provider
  • ✅ Frontend can connect via public URL
  • ⏱️ Setup time: 30 minutes

Option B: Cloud Platforms

  • Heroku - Easiest for beginners
  • Railway - Modern, simple deployment
  • Render - Free tier available
  • AWS ECS - Production-grade
  • Google Cloud Run - Serverless containers
  • DigitalOcean App Platform - Simple and affordable

Option C: Oracle Cloud (As per LLD)

  • ✅ Free tier available
  • ✅ Production-ready
  • ⏱️ Setup time: 2-3 hours

2. Docker Hub Publishing

Step 1: Create Dockerfile for Production

Create hexfeed-backend/Dockerfile:

# Multi-stage build for optimal image size
FROM maven:3.9.11-eclipse-temurin-17 AS builder

# Set working directory
WORKDIR /app

# Copy pom.xml and download dependencies (cached layer)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Copy source code
COPY src ./src

# Build application (skip tests for faster build)
RUN mvn clean package -DskipTests

# Production stage
FROM eclipse-temurin:17-jre-alpine

# Add metadata
LABEL maintainer="your-email@example.com"
LABEL description="Hexagon Feed System Backend"
LABEL version="1.0.0"

# Create non-root user
RUN addgroup -S spring && adduser -S spring -G spring

# Set working directory
WORKDIR /app

# Copy JAR from builder stage
COPY --from=builder /app/target/*.jar app.jar

# Change ownership
RUN chown -R spring:spring /app

# Switch to non-root user
USER spring:spring

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1

# Set JVM options for container
ENV JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxGCPauseMillis=200"

# Run application
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]

Step 2: Create .dockerignore

Create hexfeed-backend/.dockerignore:

target/
.mvn/
mvnw
mvnw.cmd
*.log
*.tmp
.DS_Store
.git
.gitignore
README.md
docs/
test-*.sh
*.html

Step 3: Build Docker Image

# Navigate to backend directory
cd hexfeed-backend

# Build image
docker build -t your-dockerhub-username/hexfeed-backend:latest .

# Test locally
docker run -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e DATABASE_URL=jdbc:postgresql://host.docker.internal:5432/hexfeed_db \
  -e REDIS_HOST=host.docker.internal \
  -e KAFKA_BOOTSTRAP_SERVERS=host.docker.internal:9092 \
  your-dockerhub-username/hexfeed-backend:latest

Step 4: Push to Docker Hub

# Login to Docker Hub
docker login

# Push image
docker push your-dockerhub-username/hexfeed-backend:latest

# Tag with version
docker tag your-dockerhub-username/hexfeed-backend:latest \
  your-dockerhub-username/hexfeed-backend:1.0.0
docker push your-dockerhub-username/hexfeed-backend:1.0.0

Step 5: Update docker-compose.yml for Production

Create docker-compose.production.yml:

version: '3.8'

services:
  # Backend application
  hexfeed-backend:
    image: your-dockerhub-username/hexfeed-backend:latest
    container_name: hexfeed-app
    ports:
      - "8080:8080"
    environment:
      SPRING_PROFILES_ACTIVE: prod
      DATABASE_URL: jdbc:postgresql://postgres:5432/hexfeed_db
      DATABASE_USERNAME: hexfeed_user
      DATABASE_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_PORT: 6379
      KAFKA_BOOTSTRAP_SERVERS: kafka:9092
      JWT_SECRET: ${JWT_SECRET}
    depends_on:
      - postgres
      - redis
      - kafka
    networks:
      - hexfeed-network
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 60s

  # PostgreSQL Database
  postgres:
    image: postgres:15-alpine
    container_name: hexfeed-postgres
    environment:
      POSTGRES_DB: hexfeed_db
      POSTGRES_USER: hexfeed_user
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - hexfeed-network
    restart: unless-stopped

  # Redis Cache
  redis:
    image: redis:7-alpine
    container_name: hexfeed-redis
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    networks:
      - hexfeed-network
    restart: unless-stopped
    command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}

  # Kafka + Zookeeper
  zookeeper:
    image: confluentinc/cp-zookeeper:7.4.0
    container_name: hexfeed-zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    networks:
      - hexfeed-network
    restart: unless-stopped

  kafka:
    image: confluentinc/cp-kafka:7.4.0
    container_name: hexfeed-kafka
    depends_on:
      - zookeeper
    ports:
      - "9092:9092"
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
    volumes:
      - kafka_data:/var/lib/kafka/data
    networks:
      - hexfeed-network
    restart: unless-stopped

  # Nginx Reverse Proxy (Optional, for SSL)
  nginx:
    image: nginx:alpine
    container_name: hexfeed-nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - hexfeed-backend
    networks:
      - hexfeed-network
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
  kafka_data:

networks:
  hexfeed-network:
    driver: bridge

Create .env file:

DB_PASSWORD=your_secure_password_here
REDIS_PASSWORD=your_redis_password_here
JWT_SECRET=your_jwt_secret_minimum_32_characters_long

3. Cloud Deployment

Option A: Heroku Deployment

Step 1: Create heroku.yml

build:
  docker:
    web: hexfeed-backend/Dockerfile
run:
  web: java -jar /app/app.jar

Step 2: Deploy to Heroku

# Install Heroku CLI
# Mac: brew install heroku/brew/heroku
# Windows: Download from heroku.com

# Login
heroku login

# Create app
heroku create hexfeed-backend

# Add PostgreSQL
heroku addons:create heroku-postgresql:mini

# Add Redis
heroku addons:create heroku-redis:mini

# Set environment variables
heroku config:set SPRING_PROFILES_ACTIVE=prod
heroku config:set JWT_SECRET=your_jwt_secret_here

# Deploy
git push heroku main

# View logs
heroku logs --tail

# Open app
heroku open

Option B: Railway Deployment

Step 1: Create railway.json

{
  "$schema": "https://railway.app/railway.schema.json",
  "build": {
    "builder": "DOCKERFILE",
    "dockerfilePath": "hexfeed-backend/Dockerfile"
  },
  "deploy": {
    "startCommand": "java -jar /app/app.jar",
    "healthcheckPath": "/actuator/health",
    "restartPolicyType": "ON_FAILURE"
  }
}

Step 2: Deploy

# Install Railway CLI
npm i -g @railway/cli

# Login
railway login

# Initialize project
railway init

# Add PostgreSQL
railway add --database postgresql

# Add Redis
railway add --database redis

# Deploy
railway up

# View logs
railway logs

Option C: DigitalOcean App Platform

Step 1: Create .do/app.yaml

name: hexfeed-backend
region: nyc
services:
  - name: api
    dockerfile_path: hexfeed-backend/Dockerfile
    github:
      repo: your-username/hexfeed-backend
      branch: main
    health_check:
      http_path: /actuator/health
    http_port: 8080
    instance_count: 2
    instance_size_slug: basic-xs
    routes:
      - path: /
    envs:
      - key: SPRING_PROFILES_ACTIVE
        value: prod
      - key: JWT_SECRET
        value: ${JWT_SECRET}
        type: SECRET

databases:
  - name: hexfeed-db
    engine: PG
    version: "15"
    size: db-s-1vcpu-1gb
  - name: hexfeed-redis
    engine: REDIS
    version: "7"
    size: db-s-1vcpu-1gb

Step 2: Deploy via CLI or UI

# Install doctl
# Mac: brew install doctl

# Authenticate
doctl auth init

# Create app
doctl apps create --spec .do/app.yaml

# Or use Web UI: https://cloud.digitalocean.com/apps

4. API Documentation Export

Step 1: Add Swagger Dependencies

Add to pom.xml:

<!-- Swagger/OpenAPI Documentation -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>

Step 2: Create OpenAPI Configuration

Create hexfeed-backend/src/main/java/com/hexfeed/config/OpenApiConfig.java:

package com.hexfeed.config;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class OpenApiConfig {

    @Value("${server.port:8080}")
    private String serverPort;

    @Bean
    public OpenAPI hexFeedOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("Hexagon Feed System API")
                .description("Location-based social feed using H3 hexagonal spatial indexing")
                .version("1.0.0")
                .contact(new Contact()
                    .name("HexFeed Team")
                    .email("support@hexfeed.com")
                    .url("https://hexfeed.com"))
                .license(new License()
                    .name("MIT License")
                    .url("https://opensource.org/licenses/MIT")))
            .servers(List.of(
                new Server()
                    .url("http://localhost:" + serverPort)
                    .description("Local Development"),
                new Server()
                    .url("https://api.hexfeed.com")
                    .description("Production")))
            .addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
            .components(new io.swagger.v3.oas.models.Components()
                .addSecuritySchemes("Bearer Authentication",
                    new SecurityScheme()
                        .type(SecurityScheme.Type.HTTP)
                        .scheme("bearer")
                        .bearerFormat("JWT")
                        .description("Enter JWT token")));
    }
}

Step 3: Add Swagger Annotations to Controllers

Example for FeedController.java:

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;

@RestController
@RequestMapping("/api/v1/feed")
@Tag(name = "Feed", description = "Location-based feed endpoints")
public class FeedController {

    @GetMapping
    @Operation(
        summary = "Get location-based feed",
        description = "Retrieves posts from the user's location and surrounding hexagons",
        security = @SecurityRequirement(name = "Bearer Authentication")
    )
    @ApiResponses(value = {
        @ApiResponse(
            responseCode = "200",
            description = "Feed retrieved successfully",
            content = @Content(schema = @Schema(implementation = FeedResponse.class))
        ),
        @ApiResponse(
            responseCode = "401",
            description = "Unauthorized - Invalid or missing JWT token"
        ),
        @ApiResponse(
            responseCode = "400",
            description = "Invalid request parameters"
        )
    })
    public ResponseEntity<ApiResponse<FeedResponse>> getFeed(
        @Parameter(description = "Latitude (-90 to 90)", required = true)
        @RequestParam Double latitude,
        
        @Parameter(description = "Longitude (-180 to 180)", required = true)
        @RequestParam Double longitude,
        
        @Parameter(description = "Page number (starts from 1)", example = "1")
        @RequestParam(defaultValue = "1") int page,
        
        @Parameter(description = "Number of posts per page (1-50)", example = "20")
        @RequestParam(defaultValue = "20") int limit,
        
        @AuthenticationPrincipal UserDetails userDetails
    ) {
        // Implementation
    }
}

Step 4: Access Swagger UI

# Start application
./mvnw spring-boot:run

# Open Swagger UI in browser
open http://localhost:8080/swagger-ui.html

# OpenAPI JSON specification
curl http://localhost:8080/v3/api-docs > openapi.json

5. Frontend Integration

Backend Configuration for Frontend

Step 1: Update CORS in SecurityConfig.java

@Bean
public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    
    // Allow your frontend origins
    configuration.setAllowedOriginPatterns(Arrays.asList(
        "http://localhost:3000",           // React dev
        "http://localhost:4200",           // Angular dev
        "http://localhost:5173",           // Vite dev
        "http://localhost:8081",           // Vue dev
        "https://yourdomain.com",          // Production
        "https://*.vercel.app",            // Vercel deployments
        "https://*.netlify.app",           // Netlify deployments
        "https://*.railway.app"            // Railway deployments
    ));
    
    configuration.setAllowedMethods(Arrays.asList(
        "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"
    ));
    
    configuration.setAllowedHeaders(Arrays.asList(
        "Authorization",
        "Content-Type",
        "X-Requested-With",
        "Accept",
        "Origin",
        "Access-Control-Request-Method",
        "Access-Control-Request-Headers"
    ));
    
    configuration.setExposedHeaders(Arrays.asList(
        "X-Correlation-ID",
        "X-RateLimit-Remaining",
        "X-RateLimit-Retry-After"
    ));
    
    configuration.setAllowCredentials(true);
    configuration.setMaxAge(3600L);
    
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    
    return source;
}

Step 2: Create API Client Environment File

Create hexfeed-backend/api-config.json:

{
  "development": {
    "apiUrl": "http://localhost:8080",
    "wsUrl": "ws://localhost:8080/ws",
    "timeout": 30000
  },
  "production": {
    "apiUrl": "https://api.hexfeed.com",
    "wsUrl": "wss://api.hexfeed.com/ws",
    "timeout": 30000
  }
}

6. Testing with Frontend

React Frontend Example

Step 1: Create API Service

Create src/services/api.js:

import axios from 'axios';

const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8080';

// Create axios instance
const api = axios.create({
  baseURL: `${API_URL}/api/v1`,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor - Add JWT token
api.interceptors.request.use(
  (config) => {
    const token = localStorage.getItem('accessToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor - Handle errors
api.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // Token expired - redirect to login
      localStorage.removeItem('accessToken');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

// API methods
export const authAPI = {
  register: (data) => api.post('/auth/register', data),
  login: (data) => api.post('/auth/login', data),
  refresh: (refreshToken) => api.post('/auth/refresh', { refreshToken }),
  verify: () => api.get('/auth/verify'),
};

export const feedAPI = {
  getFeed: (latitude, longitude, page = 1, limit = 20) =>
    api.get('/feed', {
      params: { latitude, longitude, page, limit },
    }),
  subscribeToFeed: (latitude, longitude) => {
    // WebSocket subscription handled separately
  },
};

export const postAPI = {
  createPost: (data) => api.post('/posts', data),
  getUserPosts: (page = 0, size = 20) =>
    api.get('/posts/user', {
      params: { page, size },
    }),
  deletePost: (postId) => api.delete(`/posts/${postId}`),
};

export default api;

Step 2: Create WebSocket Service

Create src/services/websocket.js:

import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

const WS_URL = process.env.REACT_APP_WS_URL || 'http://localhost:8080/ws';

class WebSocketService {
  constructor() {
    this.client = null;
    this.connected = false;
    this.subscriptions = {};
  }

  connect(accessToken) {
    return new Promise((resolve, reject) => {
      // Create STOMP client with SockJS
      this.client = new Client({
        webSocketFactory: () => new SockJS(WS_URL),
        connectHeaders: {
          Authorization: `Bearer ${accessToken}`,
        },
        debug: (str) => {
          console.log('STOMP:', str);
        },
        reconnectDelay: 5000,
        heartbeatIncoming: 30000,
        heartbeatOutgoing: 30000,
        onConnect: () => {
          console.log('WebSocket connected');
          this.connected = true;
          resolve();
        },
        onStompError: (frame) => {
          console.error('STOMP error:', frame);
          this.connected = false;
          reject(frame);
        },
        onWebSocketError: (error) => {
          console.error('WebSocket error:', error);
          this.connected = false;
          reject(error);
        },
      });

      this.client.activate();
    });
  }

  subscribeToLocation(latitude, longitude, callback) {
    if (!this.connected || !this.client) {
      throw new Error('WebSocket not connected');
    }

    // Send subscription request
    this.client.publish({
      destination: '/app/subscribe',
      body: JSON.stringify({ latitude, longitude }),
    });

    // Subscribe to personal feed queue
    const subscription = this.client.subscribe(
      '/user/queue/feed',
      (message) => {
        const data = JSON.parse(message.body);
        callback(data);
      }
    );

    this.subscriptions['feed'] = subscription;
    return subscription;
  }

  unsubscribe(key) {
    if (this.subscriptions[key]) {
      this.subscriptions[key].unsubscribe();
      delete this.subscriptions[key];
    }
  }

  disconnect() {
    if (this.client) {
      this.client.deactivate();
      this.connected = false;
      this.subscriptions = {};
    }
  }
}

export default new WebSocketService();

Step 3: Create React Hook for Feed

Create src/hooks/useFeed.js:

import { useState, useEffect, useCallback } from 'react';
import { feedAPI } from '../services/api';
import websocketService from '../services/websocket';

export const useFeed = (latitude, longitude) => {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [page, setPage] = useState(1);
  const [hasMore, setHasMore] = useState(true);

  // Fetch feed
  const fetchFeed = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await feedAPI.getFeed(latitude, longitude, page, 20);
      
      if (response.success) {
        if (page === 1) {
          setPosts(response.data.posts);
        } else {
          setPosts((prev) => [...prev, ...response.data.posts]);
        }
        setHasMore(response.data.pagination.hasNext);
      }
    } catch (err) {
      setError(err.message);
      console.error('Error fetching feed:', err);
    } finally {
      setLoading(false);
    }
  }, [latitude, longitude, page]);

  // Subscribe to real-time updates
  useEffect(() => {
    const token = localStorage.getItem('accessToken');
    if (!token) return;

    websocketService
      .connect(token)
      .then(() => {
        websocketService.subscribeToLocation(
          latitude,
          longitude,
          (newPost) => {
            // Prepend new post to feed
            setPosts((prev) => [newPost, ...prev]);
          }
        );
      })
      .catch((err) => {
        console.error('WebSocket connection failed:', err);
      });

    return () => {
      websocketService.unsubscribe('feed');
    };
  }, [latitude, longitude]);

  // Load initial feed
  useEffect(() => {
    fetchFeed();
  }, [fetchFeed]);

  const loadMore = () => {
    if (!loading && hasMore) {
      setPage((prev) => prev + 1);
    }
  };

  const refresh = () => {
    setPage(1);
    setPosts([]);
    fetchFeed();
  };

  return {
    posts,
    loading,
    error,
    hasMore,
    loadMore,
    refresh,
  };
};

Step 4: Create Feed Component

Create src/components/Feed.jsx:

import React, { useState, useEffect } from 'react';
import { useFeed } from '../hooks/useFeed';
import { postAPI } from '../services/api';

const Feed = () => {
  const [userLocation, setUserLocation] = useState(null);
  const [newPostContent, setNewPostContent] = useState('');

  // Get user location
  useEffect(() => {
    if ('geolocation' in navigator) {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          setUserLocation({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
          });
        },
        (error) => {
          console.error('Error getting location:', error);
          // Fallback to default location (San Francisco)
          setUserLocation({
            latitude: 37.7749,
            longitude: -122.4194,
          });
        }
      );
    }
  }, []);

  const { posts, loading, error, hasMore, loadMore, refresh } = useFeed(
    userLocation?.latitude,
    userLocation?.longitude
  );

  const handleCreatePost = async (e) => {
    e.preventDefault();
    
    if (!newPostContent.trim() || !userLocation) return;

    try {
      await postAPI.createPost({
        content: newPostContent,
        latitude: userLocation.latitude,
        longitude: userLocation.longitude,
      });
      
      setNewPostContent('');
      refresh(); // Refresh feed
    } catch (error) {
      console.error('Error creating post:', error);
      alert('Failed to create post');
    }
  };

  if (!userLocation) {
    return <div>Loading location...</div>;
  }

  return (
    <div className="feed-container">
      {/* Create Post Form */}
      <div className="create-post">
        <form onSubmit={handleCreatePost}>
          <textarea
            value={newPostContent}
            onChange={(e) => setNewPostContent(e.target.value)}
            placeholder="What's on your mind?"
            maxLength={5000}
            rows={4}
          />
          <button type="submit" disabled={!newPostContent.trim()}>
            Post
          </button>
        </form>
      </div>

      {/* Feed Posts */}
      <div className="posts">
        {error && <div className="error">Error: {error}</div>}
        
        {posts.map((post) => (
          <div key={post.id} className="post">
            <div className="post-header">
              <img
                src={post.profilePictureUrl || '/default-avatar.png'}
                alt={post.username}
                className="avatar"
              />
              <div>
                <h4>{post.displayName || post.username}</h4>
                <span className="username">@{post.username}</span>
                <span className="timestamp">
                  {new Date(post.timestamp).toLocaleString()}
                </span>
              </div>
            </div>
            
            <div className="post-content">{post.content}</div>
            
            <div className="post-footer">
              <span>{post.distanceKm.toFixed(2)} km away</span>
              <div className="post-actions">
                <button>❤️ {post.likesCount}</button>
                <button>💬 {post.commentsCount}</button>
                <button>🔄 {post.sharesCount}</button>
              </div>
            </div>
          </div>
        ))}
        
        {loading && <div className="loading">Loading...</div>}
        
        {hasMore && !loading && (
          <button onClick={loadMore} className="load-more">
            Load More
          </button>
        )}
      </div>
    </div>
  );
};

export default Feed;

Step 5: Install Dependencies

npm install axios sockjs-client @stomp/stompjs

Step 6: Create .env file

Create .env.local:

REACT_APP_API_URL=http://localhost:8080
REACT_APP_WS_URL=http://localhost:8080/ws

7. CORS Configuration

Testing CORS

# Test CORS preflight
curl -X OPTIONS http://localhost:8080/api/v1/feed \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: Authorization" \
  -v

# Expected headers in response:
# Access-Control-Allow-Origin: http://localhost:3000
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE
# Access-Control-Allow-Headers: Authorization, Content-Type
# Access-Control-Allow-Credentials: true

8. WebSocket Integration

Testing WebSocket Connection

Create test-websocket.html:

<!DOCTYPE html>
<html>
<head>
    <title>HexFeed WebSocket Test</title>
    <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@stomp/stompjs@7/bundles/stomp.umd.min.js"></script>
</head>
<body>
    <h1>HexFeed WebSocket Test</h1>
    
    <div>
        <label>JWT Token:</label><br>
        <input type="text" id="token" style="width: 500px" placeholder="Paste your JWT token here">
    </div>
    
    <div>
        <label>Latitude:</label>
        <input type="number" id="lat" value="37.7749" step="0.0001">
        <label>Longitude:</label>
        <input type="number" id="lon" value="-122.4194" step="0.0001">
    </div>
    
    <button onclick="connect()">Connect</button>
    <button onclick="disconnect()">Disconnect</button>
    <button onclick="subscribe()">Subscribe to Location</button>
    
    <div id="status"></div>
    <div id="messages" style="margin-top: 20px; border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: scroll;"></div>
    
    <script>
        let stompClient = null;
        
        function connect() {
            const token = document.getElementById('token').value;
            if (!token) {
                alert('Please enter JWT token');
                return;
            }
            
            const socket = new SockJS('http://localhost:8080/ws');
            stompClient = new StompJs.Client({
                webSocketFactory: () => socket,
                connectHeaders: {
                    'Authorization': `Bearer ${token}`
                },
                debug: (str) => console.log(str),
                onConnect: () => {
                    document.getElementById('status').innerHTML = '<span style="color: green">Connected!</span>';
                },
                onStompError: (frame) => {
                    console.error('STOMP error:', frame);
                    document.getElementById('status').innerHTML = '<span style="color: red">Error: ' + frame.headers['message'] + '</span>';
                }
            });
            
            stompClient.activate();
        }
        
        function disconnect() {
            if (stompClient) {
                stompClient.deactivate();
                document.getElementById('status').innerHTML = '<span style="color: gray">Disconnected</span>';
            }
        }
        
        function subscribe() {
            if (!stompClient || !stompClient.connected) {
                alert('Not connected');
                return;
            }
            
            const lat = parseFloat(document.getElementById('lat').value);
            const lon = parseFloat(document.getElementById('lon').value);
            
            // Send subscription request
            stompClient.publish({
                destination: '/app/subscribe',
                body: JSON.stringify({ latitude: lat, longitude: lon })
            });
            
            // Subscribe to updates
            stompClient.subscribe('/user/queue/feed', (message) => {
                const data = JSON.parse(message.body);
                const div = document.getElementById('messages');
                div.innerHTML += `<div><strong>${new Date().toLocaleTimeString()}</strong>: ${JSON.stringify(data, null, 2)}</div><hr>`;
                div.scrollTop = div.scrollHeight;
            });
            
            document.getElementById('status').innerHTML += '<br><span style="color: blue">Subscribed to location updates</span>';
        }
    </script>
</body>
</html>

9. Example Frontend Code

Vue.js Example

<template>
  <div class="feed">
    <h1>HexFeed</h1>
    
    <!-- Create Post -->
    <div class="create-post">
      <textarea
        v-model="newPost"
        placeholder="What's happening?"
        maxlength="5000"
      ></textarea>
      <button @click="createPost" :disabled="!newPost.trim()">
        Post
      </button>
    </div>
    
    <!-- Feed -->
    <div v-if="loading" class="loading">Loading...</div>
    <div v-else-if="error" class="error">{{ error }}</div>
    <div v-else class="posts">
      <div v-for="post in posts" :key="post.id" class="post">
        <div class="post-header">
          <img :src="post.profilePictureUrl || '/avatar.png'" alt="avatar">
          <div>
            <h4>{{ post.displayName }}</h4>
            <span>@{{ post.username }}</span>
          </div>
        </div>
        <p>{{ post.content }}</p>
        <div class="post-footer">
          <span>{{ post.distanceKm }} km away</span>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, onUnmounted } from 'vue';
import axios from 'axios';
import SockJS from 'sockjs-client';
import { Client } from '@stomp/stompjs';

export default {
  name: 'Feed',
  setup() {
    const posts = ref([]);
    const loading = ref(false);
    const error = ref(null);
    const newPost = ref('');
    const location = ref({ latitude: 37.7749, longitude: -122.4194 });
    let stompClient = null;

    const api = axios.create({
      baseURL: 'http://localhost:8080/api/v1',
      headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      }
    });

    const fetchFeed = async () => {
      try {
        loading.value = true;
        const response = await api.get('/feed', {
          params: location.value
        });
        posts.value = response.data.data.posts;
      } catch (err) {
        error.value = err.message;
      } finally {
        loading.value = false;
      }
    };

    const createPost = async () => {
      try {
        await api.post('/posts', {
          content: newPost.value,
          ...location.value
        });
        newPost.value = '';
        fetchFeed();
      } catch (err) {
        alert('Failed to create post');
      }
    };

    const connectWebSocket = () => {
      const socket = new SockJS('http://localhost:8080/ws');
      stompClient = new Client({
        webSocketFactory: () => socket,
        connectHeaders: {
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        },
        onConnect: () => {
          stompClient.publish({
            destination: '/app/subscribe',
            body: JSON.stringify(location.value)
          });
          stompClient.subscribe('/user/queue/feed', (message) => {
            const newPost = JSON.parse(message.body);
            posts.value.unshift(newPost);
          });
        }
      });
      stompClient.activate();
    };

    onMounted(() => {
      if ('geolocation' in navigator) {
        navigator.geolocation.getCurrentPosition((position) => {
          location.value = {
            latitude: position.coords.latitude,
            longitude: position.coords.longitude
          };
          fetchFeed();
          connectWebSocket();
        });
      } else {
        fetchFeed();
        connectWebSocket();
      }
    });

    onUnmounted(() => {
      if (stompClient) {
        stompClient.deactivate();
      }
    });

    return {
      posts,
      loading,
      error,
      newPost,
      createPost
    };
  }
};
</script>

10. Complete Testing Workflow

Step 1: Start Backend

# Terminal 1: Start Docker services
docker-compose up -d

# Terminal 2: Start backend
cd hexfeed-backend
./mvnw spring-boot:run

Step 2: Test Backend APIs

# Register user
curl -X POST http://localhost:8080/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "email": "test@example.com",
    "password": "Test@123456"
  }'

# Login and save token
TOKEN=$(curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "password": "Test@123456"
  }' | jq -r '.data.access_token')

# Create post
curl -X POST http://localhost:8080/api/v1/posts \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Hello from API!",
    "latitude": 37.7749,
    "longitude": -122.4194
  }'

# Get feed
curl "http://localhost:8080/api/v1/feed?latitude=37.7749&longitude=-122.4194" \
  -H "Authorization: Bearer $TOKEN" | jq

Step 3: Start Frontend

# Terminal 3: Start React app
cd frontend
npm install
npm start

# Or Vue.js
npm run dev

# Or Angular
ng serve

Step 4: Test Full Flow

  1. ✅ Open frontend (http://localhost:3000)
  2. ✅ Register new user
  3. ✅ Login
  4. ✅ Allow location access
  5. ✅ Create a post
  6. ✅ See post appear in feed immediately (WebSocket)
  7. ✅ Scroll to load more posts
  8. ✅ Open in multiple browsers to test real-time updates

11. Production Deployment Checklist

Backend

  • Environment variables configured
  • JWT secret changed
  • Database credentials secured
  • Redis password set
  • CORS origins updated
  • HTTPS enabled
  • Health checks working
  • Logs configured
  • Monitoring set up
  • Backup strategy in place

Frontend

  • API URL updated to production
  • WebSocket URL updated
  • Build optimized (npm run build)
  • Assets minified
  • Environment variables set
  • CDN configured (optional)
  • Error tracking (Sentry, etc.)
  • Analytics added (optional)

Testing

  • Load testing completed
  • Security audit done
  • Cross-browser testing
  • Mobile testing
  • WebSocket stability tested
  • Rate limiting tested
  • Error handling verified

🎉 Conclusion

You now have a complete guide to:

  • ✅ Publish your backend to Docker Hub or cloud platforms
  • ✅ Configure CORS and security for frontend integration
  • ✅ Implement frontend code (React, Vue, Angular)
  • ✅ Connect WebSocket for real-time updates
  • ✅ Test the complete system end-to-end

Next Steps:

  1. Choose deployment platform (Heroku, Railway, DigitalOcean)
  2. Deploy backend
  3. Create or connect frontend
  4. Test with real users

Need Help?


Happy Coding! 🚀