Project: Hexagon Feed System (HexFeed)
Last Updated: October 19, 2025
Status: Production-Ready Backend
- Publishing Options
- Docker Hub Publishing
- Cloud Deployment
- API Documentation Export
- Frontend Integration
- Testing with Frontend
- CORS Configuration
- WebSocket Integration
- Example Frontend Code
- ✅ Easy to deploy and share
- ✅ Works with any cloud provider
- ✅ Frontend can connect via public URL
- ⏱️ Setup time: 30 minutes
- 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
- ✅ Free tier available
- ✅ Production-ready
- ⏱️ Setup time: 2-3 hours
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"]Create hexfeed-backend/.dockerignore:
target/
.mvn/
mvnw
mvnw.cmd
*.log
*.tmp
.DS_Store
.git
.gitignore
README.md
docs/
test-*.sh
*.html
# 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# 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.0Create 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: bridgeCreate .env file:
DB_PASSWORD=your_secure_password_here
REDIS_PASSWORD=your_redis_password_here
JWT_SECRET=your_jwt_secret_minimum_32_characters_longbuild:
docker:
web: hexfeed-backend/Dockerfile
run:
web: java -jar /app/app.jar# 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{
"$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"
}
}# 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 logsname: 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# 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/appsAdd to pom.xml:
<!-- Swagger/OpenAPI Documentation -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>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")));
}
}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
}
}# 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@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;
}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
}
}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;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();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,
};
};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;npm install axios sockjs-client @stomp/stompjsCreate .env.local:
REACT_APP_API_URL=http://localhost:8080
REACT_APP_WS_URL=http://localhost:8080/ws# 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: trueCreate 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><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># Terminal 1: Start Docker services
docker-compose up -d
# Terminal 2: Start backend
cd hexfeed-backend
./mvnw spring-boot:run# 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# Terminal 3: Start React app
cd frontend
npm install
npm start
# Or Vue.js
npm run dev
# Or Angular
ng serve- ✅ Open frontend (http://localhost:3000)
- ✅ Register new user
- ✅ Login
- ✅ Allow location access
- ✅ Create a post
- ✅ See post appear in feed immediately (WebSocket)
- ✅ Scroll to load more posts
- ✅ Open in multiple browsers to test real-time updates
- 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
- 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)
- Load testing completed
- Security audit done
- Cross-browser testing
- Mobile testing
- WebSocket stability tested
- Rate limiting tested
- Error handling verified
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:
- Choose deployment platform (Heroku, Railway, DigitalOcean)
- Deploy backend
- Create or connect frontend
- Test with real users
Need Help?
- Check Codebase_Analysis_Report.md
- Review Project_Startup_Instructions.md
- See Swagger docs at http://localhost:8080/swagger-ui.html
Happy Coding! 🚀