What We're Building
In this advanced guide, you'll build a full-featured AI-powered route optimization system for delivery logistics. The application will let dispatchers input multiple delivery points, visualize them on an interactive map, calculate optimal routes, and use Claude AI to provide intelligent reasoning about route decisions.
Interactive Maps
Mapbox GL for real-time visualization
AI Optimization
Claude-powered route reasoning
Geospatial Data
PostGIS for location queries
Analytics
Distance, time, and cost metrics
The system addresses a real-world logistics challenge: fuel costs account for 30% of operating expenses, and inefficient routing wastes time, money, and increases carbon footprint. Our optimizer will help reduce these costs by finding the most efficient delivery sequences.
Prerequisites
Before starting this guide, make sure you have the following:
- Node.js 18+ installed on your machine
- Mapbox account with an access token (free tier available)
- Supabase account for database and PostGIS
- Anthropic API key for Claude integration
- Experience with React/Next.js and TypeScript
- Basic understanding of geospatial concepts (coordinates, distance calculation)
Tech Stack Specification
Here's the technology stack we'll use for this build:
| Layer | Technology | Why This Choice |
|---|---|---|
| Frontend | Next.js 14, Mapbox GL | Server components for performance, Mapbox for professional map visualization |
| Backend | Next.js API Routes | Unified codebase, serverless scaling, easy Vercel deployment |
| Database | Supabase + PostGIS | Managed PostgreSQL with native geospatial support for location queries |
| AI | Claude API | Advanced reasoning for route optimization and constraint handling |
| Maps | Mapbox Directions API | Professional routing data with turn-by-turn directions |
| Hosting | Vercel | Zero-config deployment, edge functions, seamless Next.js integration |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Next.js 14 Frontend β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β Map View β β Stop List β β Route Analytics β β
β β (Mapbox GL) β β Manager β β Dashboard β β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββ
β Next.js API Routes β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββββββββββ β
β β /api/ β β /api/ β β /api/ β β
β β stops β β routes β β optimize β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββββββ¬ββββββββββββ β
βββββββββββΌββββββββββββββββββΌββββββββββββββββββββββΌββββββββββββββββ
β β β
βΌ βΌ βΌ
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Supabase + β β Mapbox β β Claude API β
β PostGIS β β Directions β β (Reasoning) β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
AI Agent Workflow
Here's how to leverage AI tools throughout this build to maximize productivity:
Project Scaffolding with Claude Code
Use Claude Code to scaffold the Next.js project with all necessary configurations for Mapbox, Supabase, and the API structure.
# Claude Code prompt for project setup
Create a Next.js 14 route optimization app with:
- TypeScript configuration
- Mapbox GL integration with react-map-gl
- Supabase client setup with PostGIS types
- API routes structure for /stops, /routes, /optimize
- Tailwind CSS with a logistics-themed color scheme
- Environment variable templates for all API keys
- Type definitions for DeliveryStop, Route, and Optimization
UI Generation with v0.dev
Use v0.dev to rapidly prototype the dashboard components, including the map container, stop list sidebar, and analytics panels.
When using v0.dev for map interfaces, focus on generating the surrounding UI (sidebars, panels, controls) and manually integrate the Mapbox component. v0 excels at layout and styling but map libraries need direct configuration.
Development with Cursor
Use Cursor's AI features to help with Mapbox configuration, PostGIS query optimization, and debugging geospatial calculations. The inline AI assistance is particularly helpful for understanding GeoJSON structures.
Step-by-Step Build Guide
Phase 1: Map Interface Setup with Mapbox
Start by setting up the Next.js project and integrating Mapbox GL for our interactive map visualization.
# Create the Next.js project
npx create-next-app@latest route-optimizer --typescript --tailwind --app
cd route-optimizer
# Install Mapbox and related dependencies
npm install mapbox-gl react-map-gl @mapbox/mapbox-gl-directions
npm install @types/mapbox-gl --save-dev
# Install Supabase and Anthropic SDK
npm install @supabase/supabase-js @anthropic-ai/sdk
# Install additional utilities
npm install @turf/turf date-fns zustand
Create the main map component that will display delivery stops and routes:
'use client';
import { useRef, useCallback, useState } from 'react';
import Map, { Marker, Source, Layer, NavigationControl } from 'react-map-gl';
import type { MapRef, MarkerDragEvent } from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
interface DeliveryStop {
id: string;
name: string;
latitude: number;
longitude: number;
priority: 'high' | 'medium' | 'low';
timeWindow?: { start: string; end: string };
}
interface OptimizedRoute {
coordinates: [number, number][];
distance: number;
duration: number;
stopOrder: string[];
}
interface DeliveryMapProps {
stops: DeliveryStop[];
route: OptimizedRoute | null;
onStopDrag: (stopId: string, lat: number, lng: number) => void;
onMapClick: (lat: number, lng: number) => void;
}
export default function DeliveryMap({ stops, route, onStopDrag, onMapClick }: DeliveryMapProps) {
const mapRef = useRef<MapRef>(null);
const [viewState, setViewState] = useState({
longitude: -122.4,
latitude: 37.8,
zoom: 11
});
const handleMapClick = useCallback((event: mapboxgl.MapLayerMouseEvent) => {
const { lng, lat } = event.lngLat;
onMapClick(lat, lng);
}, [onMapClick]);
const handleMarkerDrag = useCallback((stopId: string) =>
(event: MarkerDragEvent) => {
const { lng, lat } = event.lngLat;
onStopDrag(stopId, lat, lng);
}, [onStopDrag]);
// GeoJSON for the route line
const routeGeoJSON = route ? {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: route.coordinates
}
} : null;
return (
<Map
ref={mapRef}
{...viewState}
onMove={evt => setViewState(evt.viewState)}
onClick={handleMapClick}
mapStyle="mapbox://styles/mapbox/dark-v11"
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
style={{ width: '100%', height: '100%' }}
>
<NavigationControl position="top-right" />
{/* Render route line */}
{routeGeoJSON && (
<Source id="route" type="geojson" data={routeGeoJSON}>
<Layer
id="route-line"
type="line"
paint={{
'line-color': '#f97316',
'line-width': 4,
'line-opacity': 0.8
}}
/>
</Source>
)}
{/* Render delivery stop markers */}
{stops.map((stop, index) => (
<Marker
key={stop.id}
longitude={stop.longitude}
latitude={stop.latitude}
draggable
onDragEnd={handleMarkerDrag(stop.id)}
>
<div className={`
w-8 h-8 rounded-full flex items-center justify-center
text-white font-bold text-sm shadow-lg cursor-pointer
${stop.priority === 'high' ? 'bg-red-500' :
stop.priority === 'medium' ? 'bg-orange-500' : 'bg-green-500'}
`}>
{index + 1}
</div>
</Marker>
))}
</Map>
);
}
Phase 2: Delivery Point Management
Set up Supabase with PostGIS for storing and querying delivery locations. Create the database schema and API endpoints for managing stops.
-- Enable PostGIS extension
CREATE EXTENSION IF NOT EXISTS postgis;
-- Create delivery stops table
CREATE TABLE delivery_stops (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL,
address TEXT,
location GEOGRAPHY(Point, 4326) NOT NULL,
priority TEXT CHECK (priority IN ('high', 'medium', 'low')) DEFAULT 'medium',
time_window_start TIME,
time_window_end TIME,
service_duration INTERVAL DEFAULT '15 minutes',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create index for spatial queries
CREATE INDEX idx_delivery_stops_location
ON delivery_stops USING GIST(location);
-- Create routes table
CREATE TABLE optimized_routes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
stop_ids UUID[] NOT NULL,
stop_order INTEGER[] NOT NULL,
total_distance DECIMAL(10, 2),
total_duration INTERVAL,
route_geometry GEOGRAPHY(LineString, 4326),
ai_reasoning TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Function to find nearby stops within radius
CREATE OR REPLACE FUNCTION find_nearby_stops(
center_lat DOUBLE PRECISION,
center_lng DOUBLE PRECISION,
radius_meters INTEGER
)
RETURNS TABLE (
id UUID,
name TEXT,
distance_meters DOUBLE PRECISION
) AS $$
BEGIN
RETURN QUERY
SELECT
ds.id,
ds.name,
ST_Distance(
ds.location::geography,
ST_SetSRID(ST_MakePoint(center_lng, center_lat), 4326)::geography
) AS distance_meters
FROM delivery_stops ds
WHERE ST_DWithin(
ds.location::geography,
ST_SetSRID(ST_MakePoint(center_lng, center_lat), 4326)::geography,
radius_meters
)
ORDER BY distance_meters;
END;
$$ LANGUAGE plpgsql;
Create the API route for managing delivery stops:
import { createClient } from '@supabase/supabase-js';
import { NextRequest, NextResponse } from 'next/server';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
export async function GET() {
const { data, error } = await supabase
.from('delivery_stops')
.select(`
id,
name,
address,
priority,
time_window_start,
time_window_end,
service_duration
`)
.order('created_at', { ascending: false });
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
// Fetch coordinates separately (PostGIS returns complex format)
const stopsWithCoords = await Promise.all(
data.map(async (stop) => {
const { data: coordData } = await supabase
.rpc('get_stop_coordinates', { stop_id: stop.id });
return {
...stop,
latitude: coordData?.[0]?.lat,
longitude: coordData?.[0]?.lng
};
})
);
return NextResponse.json(stopsWithCoords);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const { name, address, latitude, longitude, priority, timeWindow } = body;
// Insert with PostGIS geography point
const { data, error } = await supabase.rpc('insert_delivery_stop', {
p_name: name,
p_address: address,
p_lat: latitude,
p_lng: longitude,
p_priority: priority || 'medium',
p_time_start: timeWindow?.start || null,
p_time_end: timeWindow?.end || null
});
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
return NextResponse.json(data, { status: 201 });
}
Phase 3: Route Calculation with Mapbox
Integrate Mapbox Directions API to calculate driving routes between delivery stops, including distance and duration estimates.
interface Waypoint {
latitude: number;
longitude: number;
}
interface RouteResult {
coordinates: [number, number][];
distance: number; // meters
duration: number; // seconds
legs: RouteLeg[];
}
interface RouteLeg {
distance: number;
duration: number;
summary: string;
}
export async function getMapboxRoute(
waypoints: Waypoint[]
): Promise<RouteResult> {
if (waypoints.length < 2) {
throw new Error('At least 2 waypoints required');
}
// Format coordinates for Mapbox API: lng,lat;lng,lat;...
const coordinatesString = waypoints
.map(wp => `${wp.longitude},${wp.latitude}`)
.join(';');
const url = `https://api.mapbox.com/directions/v5/mapbox/driving/${coordinatesString}?` +
new URLSearchParams({
access_token: process.env.MAPBOX_ACCESS_TOKEN!,
geometries: 'geojson',
overview: 'full',
steps: 'true'
});
const response = await fetch(url);
const data = await response.json();
if (data.code !== 'Ok') {
throw new Error(`Mapbox API error: ${data.message}`);
}
const route = data.routes[0];
return {
coordinates: route.geometry.coordinates,
distance: route.distance,
duration: route.duration,
legs: route.legs.map((leg: any) => ({
distance: leg.distance,
duration: leg.duration,
summary: leg.summary
}))
};
}
// Calculate distance matrix for optimization
export async function getDistanceMatrix(
waypoints: Waypoint[]
): Promise<number[][]> {
const coordinatesString = waypoints
.map(wp => `${wp.longitude},${wp.latitude}`)
.join(';');
const url = `https://api.mapbox.com/directions-matrix/v1/mapbox/driving/${coordinatesString}?` +
new URLSearchParams({
access_token: process.env.MAPBOX_ACCESS_TOKEN!,
annotations: 'duration,distance'
});
const response = await fetch(url);
const data = await response.json();
return data.durations; // 2D array of travel times
}
Phase 4: AI Optimization Layer (Claude)
This is where the magic happens. We use Claude to analyze the delivery constraints, reason about optimal ordering, and provide explanations for route decisions.
import Anthropic from '@anthropic-ai/sdk';
import { NextRequest, NextResponse } from 'next/server';
import { getDistanceMatrix, getMapboxRoute } from '@/lib/mapbox';
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY!
});
interface DeliveryStop {
id: string;
name: string;
latitude: number;
longitude: number;
priority: 'high' | 'medium' | 'low';
timeWindow?: { start: string; end: string };
serviceDuration?: number; // minutes
}
export async function POST(request: NextRequest) {
const { stops, startLocation, constraints } = await request.json();
// Get distance matrix from Mapbox
const allPoints = [startLocation, ...stops];
const distanceMatrix = await getDistanceMatrix(allPoints);
// Prepare context for Claude
const stopsContext = stops.map((stop: DeliveryStop, idx: number) => ({
index: idx + 1,
name: stop.name,
priority: stop.priority,
timeWindow: stop.timeWindow || 'flexible',
serviceDuration: stop.serviceDuration || 15
}));
const matrixContext = distanceMatrix.map((row, i) =>
`From ${i === 0 ? 'depot' : `stop ${i}`}: ` +
row.map((duration, j) =>
`${j === 0 ? 'depot' : `stop ${j}`}=${Math.round(duration / 60)}min`
).join(', ')
).join('\n');
// Use Claude for intelligent route optimization
const message = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 2048,
messages: [{
role: 'user',
content: `You are a delivery route optimization expert. Analyze these delivery stops and determine the optimal order to minimize total travel time while respecting constraints.
DELIVERY STOPS:
${JSON.stringify(stopsContext, null, 2)}
TRAVEL TIME MATRIX (in minutes):
${matrixContext}
CONSTRAINTS:
- High priority stops should be visited early in the route
- Respect time windows when specified
- Account for service duration at each stop
- Vehicle starts and ends at depot (index 0)
${constraints?.maxRouteTime ? `- Maximum route duration: ${constraints.maxRouteTime} hours` : ''}
Respond with a JSON object containing:
1. "order": array of stop indices in optimal visit order (e.g., [2, 1, 4, 3])
2. "reasoning": explanation of why this order is optimal
3. "estimatedDuration": total route time in minutes
4. "warnings": any concerns or trade-offs in this route
Only respond with valid JSON.`
}]
});
// Parse Claude's response
const responseText = message.content[0].type === 'text'
? message.content[0].text
: '';
let optimization;
try {
optimization = JSON.parse(responseText);
} catch (e) {
// Extract JSON from response if wrapped in markdown
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
optimization = jsonMatch ? JSON.parse(jsonMatch[0]) : null;
}
if (!optimization) {
return NextResponse.json(
{ error: 'Failed to parse optimization result' },
{ status: 500 }
);
}
// Get actual route geometry from Mapbox
const orderedWaypoints = [
startLocation,
...optimization.order.map((idx: number) => stops[idx - 1]),
startLocation // Return to depot
];
const routeGeometry = await getMapboxRoute(orderedWaypoints);
return NextResponse.json({
optimizedRoute: {
stopOrder: optimization.order.map((idx: number) => stops[idx - 1].id),
coordinates: routeGeometry.coordinates,
distance: routeGeometry.distance,
duration: routeGeometry.duration,
legs: routeGeometry.legs
},
aiReasoning: optimization.reasoning,
warnings: optimization.warnings,
estimatedDuration: optimization.estimatedDuration
});
}
Traditional route optimization uses algorithms like TSP solvers, but Claude adds reasoning about soft constraints, priority balancing, and can explain decisions in natural language. This hybrid approach combines algorithmic efficiency with contextual understanding.
Phase 5: Real-Time Tracking Simulation
Add a simulation layer that shows vehicle progress along the route in real-time, useful for testing and demonstrations.
import { useState, useEffect, useCallback } from 'react';
import * as turf from '@turf/turf';
interface VehiclePosition {
latitude: number;
longitude: number;
bearing: number;
progress: number; // 0-1
currentLegIndex: number;
eta: Date;
}
export function useRouteSimulation(
routeCoordinates: [number, number][] | null,
speedMultiplier: number = 1
) {
const [isSimulating, setIsSimulating] = useState(false);
const [vehiclePosition, setVehiclePosition] = useState<VehiclePosition | null>(null);
const startSimulation = useCallback(() => {
if (!routeCoordinates || routeCoordinates.length < 2) return;
setIsSimulating(true);
}, [routeCoordinates]);
const stopSimulation = useCallback(() => {
setIsSimulating(false);
setVehiclePosition(null);
}, []);
useEffect(() => {
if (!isSimulating || !routeCoordinates) return;
const line = turf.lineString(routeCoordinates);
const totalLength = turf.length(line, { units: 'kilometers' });
let currentDistance = 0;
const speed = 0.5 * speedMultiplier; // km per interval
const interval = setInterval(() => {
currentDistance += speed;
const progress = Math.min(currentDistance / totalLength, 1);
if (progress >= 1) {
stopSimulation();
return;
}
// Get position along route
const point = turf.along(line, currentDistance, { units: 'kilometers' });
const coords = point.geometry.coordinates;
// Calculate bearing to next point
const nextPoint = turf.along(line, currentDistance + 0.1, { units: 'kilometers' });
const bearing = turf.bearing(point, nextPoint);
// Estimate ETA
const remainingKm = totalLength - currentDistance;
const avgSpeedKmh = 40; // Assume 40 km/h average
const etaMinutes = (remainingKm / avgSpeedKmh) * 60;
const eta = new Date(Date.now() + etaMinutes * 60000);
setVehiclePosition({
longitude: coords[0],
latitude: coords[1],
bearing,
progress,
currentLegIndex: Math.floor(progress * (routeCoordinates.length - 1)),
eta
});
}, 100);
return () => clearInterval(interval);
}, [isSimulating, routeCoordinates, speedMultiplier, stopSimulation]);
return {
isSimulating,
vehiclePosition,
startSimulation,
stopSimulation
};
}
Phase 6: Analytics and Reporting
Build a dashboard component that displays route analytics, including distance, estimated time, fuel costs, and CO2 savings compared to unoptimized routes.
'use client';
interface RouteAnalyticsProps {
optimizedRoute: {
distance: number;
duration: number;
legs: { distance: number; duration: number; summary: string }[];
} | null;
baselineDistance?: number;
aiReasoning?: string;
}
export default function RouteAnalytics({
optimizedRoute,
baselineDistance,
aiReasoning
}: RouteAnalyticsProps) {
if (!optimizedRoute) {
return (
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800">
<p className="text-gray-400">
Add stops and click "Optimize Route" to see analytics.
</p>
</div>
);
}
const distanceKm = optimizedRoute.distance / 1000;
const durationHours = optimizedRoute.duration / 3600;
const durationMinutes = (optimizedRoute.duration % 3600) / 60;
// Fuel calculations (assume 8L/100km for delivery van)
const fuelConsumption = distanceKm * 0.08;
const fuelCost = fuelConsumption * 1.50; // $1.50/L
// CO2 calculations (2.31 kg CO2 per liter of diesel)
const co2Emissions = fuelConsumption * 2.31;
// Savings vs baseline
const distanceSaved = baselineDistance
? ((baselineDistance - optimizedRoute.distance) / baselineDistance) * 100
: 0;
return (
<div className="bg-gray-900 rounded-xl p-6 border border-gray-800 space-y-6">
<h3 className="text-lg font-semibold text-white mb-4">
Route Analytics
</h3>
{/* Key Metrics Grid */}
<div className="grid grid-cols-2 gap-4">
<div className="bg-gray-800 rounded-lg p-4">
<div className="text-2xl font-bold text-orange-500">
{distanceKm.toFixed(1)} km
</div>
<div className="text-sm text-gray-400">Total Distance</div>
</div>
<div className="bg-gray-800 rounded-lg p-4">
<div className="text-2xl font-bold text-orange-500">
{Math.floor(durationHours)}h {Math.round(durationMinutes)}m
</div>
<div className="text-sm text-gray-400">Est. Duration</div>
</div>
<div className="bg-gray-800 rounded-lg p-4">
<div className="text-2xl font-bold text-green-500">
${fuelCost.toFixed(2)}
</div>
<div className="text-sm text-gray-400">Est. Fuel Cost</div>
</div>
<div className="bg-gray-800 rounded-lg p-4">
<div className="text-2xl font-bold text-green-500">
{co2Emissions.toFixed(1)} kg
</div>
<div className="text-sm text-gray-400">CO2 Emissions</div>
</div>
</div>
{/* Savings Banner */}
{distanceSaved > 0 && (
<div className="bg-green-900/30 border border-green-700 rounded-lg p-4">
<div className="flex items-center gap-2">
<span className="text-green-400 text-lg">Optimized!</span>
<span className="text-green-300">
{distanceSaved.toFixed(1)}% shorter than naive route
</span>
</div>
</div>
)}
{/* AI Reasoning */}
{aiReasoning && (
<div className="border-t border-gray-700 pt-4">
<h4 className="text-sm font-medium text-gray-300 mb-2">
AI Optimization Reasoning
</h4>
<p className="text-sm text-gray-400 leading-relaxed">
{aiReasoning}
</p>
</div>
)}
{/* Leg Breakdown */}
<div className="border-t border-gray-700 pt-4">
<h4 className="text-sm font-medium text-gray-300 mb-3">
Route Breakdown
</h4>
<div className="space-y-2">
{optimizedRoute.legs.map((leg, idx) => (
<div
key={idx}
className="flex justify-between text-sm py-2 border-b border-gray-800"
>
<span className="text-gray-400">
Leg {idx + 1}: {leg.summary || 'Direct route'}
</span>
<span className="text-gray-300">
{(leg.distance / 1000).toFixed(1)} km /
{Math.round(leg.duration / 60)} min
</span>
</div>
))}
</div>
</div>
</div>
);
}
Common Issues & Solutions
Here are some common issues you might encounter and how to solve them:
Never expose your Mapbox secret token in client-side code. Use NEXT_PUBLIC_MAPBOX_TOKEN for the public token (map display) and keep the secret token server-side only for Directions API calls.
Use GEOGRAPHY type (not GEOMETRY) for real-world coordinates. Geography uses meters for distance calculations and handles Earth's curvature correctly. Geometry uses planar math and is only accurate for small areas.
Other common issues:
- CORS errors with Mapbox: Ensure API calls to Mapbox Directions go through your Next.js API routes, not directly from the client
- Claude JSON parsing: Always handle cases where Claude wraps JSON in markdown code blocks; use regex to extract the JSON
- Rate limits: Implement caching for Mapbox distance matrix results to avoid hitting API limits
- Large stop counts: For routes with 25+ stops, batch the optimization or use a hybrid approach with traditional algorithms
Next Steps
Congratulations on building your AI Route Optimizer! Here are some ways to extend the project:
- Add vehicle constraints: Implement capacity limits, vehicle types, and driver schedules
- Real-time traffic: Integrate Mapbox Traffic API for dynamic route updates
- Multi-vehicle routing: Extend to support fleet optimization with multiple drivers
- Mobile driver app: Build a React Native companion app for drivers with turn-by-turn navigation
- Historical analytics: Track route performance over time and identify optimization opportunities
Follow the Vibe Coding Enthusiast
Follow JD β product updates on LinkedIn, personal takes on X.