This document describes how the Grainlify frontend integrates with the Patchwork backend API.
The backend URL is configured via environment variables and centralized in src/shared/config/api.ts:
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
export const FRONTEND_BASE_URL = import.meta.env.VITE_FRONTEND_BASE_URL || window.location.origin;Never hardcode the backend URL. Always use the VITE_API_BASE_URL environment variable.
Set the backend URL in your .env file:
VITE_API_BASE_URL=http://localhost:8080
VITE_FRONTEND_BASE_URL=http://localhost:5173For production, update .env or configure environment variables in your deployment platform:
VITE_API_BASE_URL=https://api.grainlify.com
VITE_FRONTEND_BASE_URL=https://grainlify.comGrainlify uses GitHub OAuth for authentication. There is no email/password authentication.
1. User clicks "Sign In" or "Sign Up"
↓
2. Frontend redirects to: {VITE_API_BASE_URL}/auth/github/login/start
↓
3. Backend redirects to GitHub OAuth
↓
4. User authorizes Grainlify
↓
5. GitHub redirects to backend callback
↓
6. Backend processes OAuth and redirects to: {VITE_FRONTEND_BASE_URL}/auth/callback?token=<jwt>
↓
7. Frontend extracts token, stores in localStorage, fetches user info
↓
8. User is redirected to /dashboard
Sign In/Sign Up Pages (src/features/auth/pages/SignInPage.tsx, SignUpPage.tsx)
- Both redirect to the same GitHub OAuth flow
- No role selection needed upfront (backend assigns roles)
OAuth Callback Handler (src/features/auth/pages/AuthCallbackPage.tsx)
- Extracts JWT from URL query parameter:
?token=<jwt> - Stores token in localStorage as
patchwork_jwt - Calls
login(token)from AuthContext - Redirects to
/dashboard
Authentication Context (src/app/contexts/AuthContext.tsx)
- Manages auth state:
isAuthenticated,userRole,userId - Provides
login(token)andlogout()methods - Fetches user info from
/meendpoint after login
JWT tokens are stored in localStorage with the key patchwork_jwt:
// Store token
localStorage.setItem('patchwork_jwt', token);
// Retrieve token
const token = localStorage.getItem('patchwork_jwt');
// Clear token
localStorage.removeItem('patchwork_jwt');Security Considerations:
⚠️ localStorage is vulnerable to XSS attacks- ✅ Suitable for development and prototyping
⚠️ For production, consider httpOnly cookies- ✅ Never log or expose tokens in console
- ✅ Clear tokens on logout
All authenticated API requests include the JWT in the Authorization header:
Authorization: Bearer <jwt_token>The API client (src/shared/api/client.ts) automatically includes this header:
const token = localStorage.getItem('patchwork_jwt');
const response = await fetch(`${API_BASE_URL}/endpoint`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});If the backend returns 401 Unauthorized:
- Token is expired or invalid
- Frontend clears the token
- User is redirected to sign-in page
All API calls are centralized in src/shared/api/client.ts.
import { getCurrentUser, getUserProfile, getPublicProjects } from '@/shared/api/client';
// Get current authenticated user
const user = await getCurrentUser();
console.log(user.id, user.role); // UUID, 'contributor' | 'maintainer' | 'admin'
// Get user profile with contributions
const profile = await getUserProfile();
console.log(profile.contributions_count, profile.languages);
// Get public projects with filters
const projects = await getPublicProjects({
language: 'TypeScript',
ecosystem: 'Starknet',
limit: 20,
});getCurrentUser()→ Get current user info (id, role)getGitHubLoginUrl()→ Get GitHub OAuth start URLgetGitHubStatus()→ Check if GitHub account is linked
getUserProfile()→ Get contributions, languages, ecosystemsgetProfileCalendar()→ Get 365-day contribution calendargetProfileActivity(limit, offset)→ Get paginated activity feed
getPublicProjects(params)→ Get filtered project listgetProjectFilters()→ Get available filters (languages, tags, ecosystems)getMyProjects()→ Get projects owned by user (maintainers only)createProject(data)→ Create a new projectverifyProject(id)→ Verify project ownershipsyncProject(id)→ Sync project data from GitHub
getEcosystems()→ Get list of ecosystems
startKYCVerification()→ Start KYC verification sessiongetKYCStatus()→ Get KYC verification status
The API client automatically handles errors:
401 Unauthorized - Token expired/invalid
// Automatically clears token and redirects to sign-in
localStorage.removeItem('patchwork_jwt');
window.location.href = '/auth/signin';Other Errors - Throws error with backend message
try {
const profile = await getUserProfile();
} catch (error) {
console.error(error.message); // Backend error message
// Handle error (show toast, etc.)
}The backend must allow requests from your frontend domain.
Development:
- Backend should allow
http://localhost:5173(or your Vite dev server port) - Or configure CORS to allow all origins (development only)
Production:
- Backend should allow
https://your-frontend-domain.com - Use specific origins, not wildcard (
*)
/src
├── shared/
│ ├── api/
│ │ ├── client.ts # API client with all endpoints
│ │ └── index.ts # Exports
│ └── config/
│ └── api.ts # API configuration (VITE_API_BASE_URL)
├── app/
│ └── contexts/
│ └── AuthContext.tsx # Authentication state management
└── features/
└── auth/
└── pages/
├── AuthCallbackPage.tsx # OAuth callback handler
├── SignInPage.tsx # GitHub OAuth sign-in
└── SignUpPage.tsx # GitHub OAuth sign-up
All environment variables must use the VITE_ prefix to be exposed to the browser.
.env
# Backend API base URL (required)
VITE_API_BASE_URL=http://localhost:8080
# Frontend base URL (optional, defaults to window.location.origin)
VITE_FRONTEND_BASE_URL=http://localhost:5173src/shared/config/api.ts
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080';
export const FRONTEND_BASE_URL = import.meta.env.VITE_FRONTEND_BASE_URL || window.location.origin;✅ Do:
- Store JWT in localStorage as
patchwork_jwt - Clear token on 401 response
- Never log tokens to console
- Use HTTPS in production
- Validate token presence before authenticated requests
- localStorage is vulnerable to XSS attacks
- JWT payload is readable (don't store sensitive data)
- Tokens persist across browser sessions (no automatic expiry on close)
The backend must:
- ✅ Keep OAuth client secrets secure (never expose to frontend)
- ✅ Set appropriate JWT expiration times
- ✅ Validate JWTs on every request
- ✅ Use HTTPS in production
- ✅ Configure CORS appropriately
- ✅ Never expose
ADMIN_BOOTSTRAP_TOKENto frontend
For production deployments, consider:
- Using httpOnly cookies instead of localStorage
- Implementing refresh token rotation
- Adding CSRF protection
- Setting up rate limiting
- Enabling security headers (CSP, HSTS, etc.)
- ✅ Check backend is running at
VITE_API_BASE_URL - ✅ Verify CORS is configured correctly
- ✅ Check browser console for detailed errors
- ✅ Ensure
.envfile exists and is loaded
- ✅ Verify backend's
PUBLIC_BASE_URLincludes your frontend URL - ✅ Check
/auth/callbackroute is registered in React Router - ✅ Ensure GitHub OAuth app callback URL matches backend callback
- ✅ Check backend's JWT expiration settings
- ✅ Verify token is stored correctly in localStorage
- ✅ Check browser console for 401 responses
- ✅ Backend must allow requests from
VITE_FRONTEND_BASE_URL - ✅ Check backend CORS configuration includes your frontend domain
- ✅ Verify preflight OPTIONS requests are handled
- ✅ Authentication (GitHub OAuth)
- ✅ Profile pages with real API data
- ✅ Projects browsing with filters
- ✅ Ecosystems integration
- ✅ Maintainer dashboard
- ⬜ KYC verification flow
- ⬜ Admin panel (admin role)
- ⬜ Real-time notifications (WebSocket/SSE)
- ⬜ Payment processing integration
- Start backend: Ensure it's running at
VITE_API_BASE_URL - Start frontend:
pnpm run dev - Click "Sign In" or "Sign Up"
- Authorize on GitHub
- Verify redirect to
/auth/callbackwith token - Verify redirect to
/dashboard - Check localStorage contains
patchwork_jwt - Verify API requests include Authorization header
The backend issues a JWT whose payload includes the user's role field. The frontend reads this via AuthContext and exposes it as userRole (type 'contributor' | 'maintainer' | 'admin' | null).
| Role | Permitted views |
|---|---|
contributor |
All standard dashboard pages |
maintainer |
All contributor views + Maintainers dashboard (/dashboard/maintainers) |
admin |
All maintainer views + Admin panel (/dashboard/admin) + Data page |
src/shared/components/RoleGuard.tsx provides a <RoleGuard allow={[...roles]}> component that renders children only when the current userRole is listed in allow. Unauthorized roles see a themed "Access Restricted" state.
// Only admins can reach this subtree
<RoleGuard allow={['admin']}>
<AdminPage />
</RoleGuard>
// Maintainers and admins
<RoleGuard allow={['maintainer', 'admin']}>
<MaintainersPage />
</RoleGuard>Client-side role checks are a UX guard only. They prevent accidental navigation to wrong views and surface clear "no access" states, but they do not constitute a security boundary. The backend must independently enforce authorization on every admin/maintainer endpoint — do not rely on the frontend guard to protect sensitive data or mutations.