BioForge uses Supabase for backend operations. The API is divided into:
- Authentication - Supabase Auth
- Database - Supabase PostgREST
- Storage - Supabase Storage
- Custom Endpoints - Astro API routes
- App:
http://localhost:4321 - Supabase:
http://localhost:54321
- App:
https://your-domain.vercel.app - Supabase:
https://your-project.supabase.co
Register a new user account.
Request Body:
{
"username": "string (3-20 chars, lowercase, numbers, underscores)",
"email": "string (valid email)",
"password": "string (min 8 chars)"
}Success Response: 200 OK
{
"success": true
}Error Response: 400 Bad Request
{
"error": "Error message"
}Side Effects:
- Creates auth user in Supabase
- Creates profile in
profilestable - Sets authentication cookies
Log in an existing user.
Request Body:
{
"email": "string",
"password": "string"
}Success Response: 200 OK
{
"success": true
}Error Response: 401 Unauthorized
{
"error": "Invalid email or password"
}Side Effects:
- Sets authentication cookies
Log out the current user.
Success Response: 302 Redirect
- Redirects to
/login
Side Effects:
- Clears authentication cookies
Track a link click and redirect to target URL.
Query Parameters:
id- Link UUIDurl- Target URL (URL-encoded)
Success Response: 302 Redirect
- Redirects to target URL
Side Effects:
- Increments click count via
increment_link_click()RPC
User profile information.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
user_id |
UUID | Foreign key to auth.users |
username |
TEXT | Unique username |
display_name |
TEXT | Display name (nullable) |
bio |
TEXT | Bio (max 160 chars, nullable) |
avatar_url |
TEXT | Avatar URL (nullable) |
theme |
JSONB | Theme settings |
plan |
TEXT | Subscription plan |
is_active |
BOOLEAN | Account active status |
created_at |
TIMESTAMP | Creation date |
updated_at |
TIMESTAMP | Last update date |
Constraints:
- Username:
/^[a-z0-9_]{3,20}$/ - Bio: Max 160 characters
- Display name: Max 50 characters
Indexes:
user_id(unique)username(unique)is_active
User links to display on profile.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
profile_id |
UUID | Foreign key to profiles |
title |
TEXT | Link title (max 100 chars) |
url |
TEXT | Link URL (must be http/https) |
position |
INTEGER | Display order |
active |
BOOLEAN | Active status |
created_at |
TIMESTAMP | Creation date |
updated_at |
TIMESTAMP | Last update date |
Constraints:
- Title: Max 100 characters
- URL: Must match
/^https?://
Indexes:
profile_idactiveposition
Aggregated daily profile view counts.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
profile_id |
UUID | Foreign key to profiles |
date |
DATE | Stats date |
views |
INTEGER | View count |
Unique Constraint: (profile_id, date)
Indexes:
profile_iddate
Aggregated daily link click counts.
| Column | Type | Description |
|---|---|---|
id |
UUID | Primary key |
link_id |
UUID | Foreign key to links |
date |
DATE | Stats date |
clicks |
INTEGER | Click count |
Unique Constraint: (link_id, date)
Indexes:
link_iddate
Increment the view count for a profile.
Parameters:
p_username- Profile username
Returns: void
Side Effects:
- Inserts or updates
daily_profile_statsfor today
Example:
const { error } = await supabase.rpc('increment_profile_view', {
p_username: 'johndoe'
});Increment the click count for a link.
Parameters:
p_link_id- Link UUID
Returns: void
Side Effects:
- Inserts or updates
daily_link_statsfor today
Example:
const { error } = await supabase.rpc('increment_link_click', {
p_link_id: 'uuid-here'
});Public SELECT:
- Anyone can view active profiles (
is_active = true)
Authenticated SELECT:
- Users can view their own profile
Authenticated UPDATE:
- Users can update their own profile
Authenticated INSERT:
- Users can create their own profile
Public SELECT:
- Anyone can view active links of active profiles
Authenticated SELECT:
- Users can view all their own links
Authenticated INSERT:
- Users can create links for their profile
Authenticated UPDATE:
- Users can update their own links
Authenticated DELETE:
- Users can delete their own links
Authenticated SELECT:
- Users can view stats for their own profile/links
No Direct Modifications:
- All modifications must go through RPC functions
Public bucket for user avatars.
Policies:
- SELECT: Public read access
- INSERT: Authenticated users only
- UPDATE: Authenticated users only
- DELETE: Authenticated users only
Usage:
// Upload avatar
const { data, error } = await supabase.storage
.from('avatars')
.upload('user-id.jpg', file, { upsert: true });
// Get public URL
const { data: { publicUrl } } = supabase.storage
.from('avatars')
.getPublicUrl('user-id.jpg');- No hard limit (uses caching)
- Cached for 60 seconds per profile
- No hard limit (fire-and-forget tracking)
- Follows Supabase plan limits
- Free tier: ~500 concurrent connections
| Code | Description |
|---|---|
| 200 | Success |
| 302 | Redirect |
| 400 | Bad Request (validation error) |
| 401 | Unauthorized (auth required) |
| 403 | Forbidden (RLS policy violation) |
| 404 | Not Found |
| 500 | Internal Server Error |
// This happens automatically on registration
// via /api/register endpointconst { data, error } = await supabase
.from('profiles')
.update({
display_name: 'John Doe',
bio: 'Content creator and streamer',
theme: {
primary_color: '#3b82f6',
background_color: '#1e293b'
}
})
.eq('user_id', user.id);const { data, error } = await supabase
.from('links')
.insert({
profile_id: profileId,
title: 'YouTube Channel',
url: 'https://youtube.com/@mychannel',
position: 0
});const { data, error } = await supabase
.from('profiles')
.select(`
*,
links:links(*)
`)
.eq('username', 'johndoe')
.eq('is_active', true)
.single();- Always validate input on both client and server
- Use RPC functions for analytics (never direct INSERT)
- Respect RLS policies - don't try to bypass them
- Cache profile pages appropriately
- Use indexes for optimal query performance
- Handle errors gracefully with user-friendly messages
For more information, see the README and source code.