This document provides comprehensive documentation for all public APIs, functions, and components in the Wrk.so portfolio platform.
- Authentication System
- Database Schema
- API Routes
- Server Actions
- React Components
- Custom Hooks
- Utilities & Helpers
- Data Layer
- TypeScript Types
The authentication system uses Better Auth with OAuth providers and email/password authentication.
import { auth } from "@/lib/auth";
// Usage in API routes
const session = await auth.api.getSession({ headers: await headers() });
// Usage in components
const { data: session } = await auth.api.getSession();Features:
- GitHub OAuth
- Google OAuth
- Email/password authentication
- Username validation with reserved username protection
- Polar integration for subscriptions
- 30-day session duration
Environment Variables Required:
GITHUB_CLIENT_ID=your_github_client_id
GITHUB_CLIENT_SECRET=your_github_client_secret
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
POLAR_ACCESS_TOKEN=your_polar_access_token
POLAR_WEBHOOK_SECRET=your_polar_webhook_secret
BETTER_AUTH_URL=your_app_urlClient-side authentication utilities.
import { authClient, signIn, signUp, useSession } from "@/lib/auth-client";
// Sign in
await signIn.email({
email: "user@example.com",
password: "password123",
});
// Sign up
await signUp.email({
email: "user@example.com",
password: "password123",
name: "John Doe",
});
// Use session hook
const { data: session, isLoading } = useSession();// Type definition
type User = {
id: string;
name: string;
email: string;
emailVerified: boolean;
image?: string;
username: string;
displayUsername?: string;
createdAt: Date;
updatedAt: Date;
// Subscription fields
polarCustomerId?: string;
subscriptionStatus?: string;
subscriptionId?: string;
subscriptionProductId?: string;
subscriptionCurrentPeriodEnd?: Date;
// Custom domain fields (Pro only)
customDomain?: string;
domainStatus?:
| "pending"
| "dns_configured"
| "vercel_pending"
| "ssl_pending"
| "active"
| "error";
domainErrorMessage?: string;
domainVerifiedAt?: Date;
};// Type definition
type Project = {
id: string;
title: string;
about?: string;
slug: string;
externalLink?: string;
featuredImageId?: string;
imageIds: string[];
displayOrder: number;
createdAt: Date;
updatedAt: Date;
userId: string;
};// Type definition
type Profile = {
id: string;
userId: string;
title?: string;
bio?: string;
location?: string;
profileImageId?: string;
createdAt: Date;
updatedAt: Date;
};// Type definition
type Media = {
id: string;
url: string;
width: number;
height: number;
alt?: string;
size?: number;
mimeType?: string;
projectId?: string;
createdAt: Date;
updatedAt: Date;
};// Type definition
type Theme = {
id: string;
userId: string;
gridType: "masonry" | "grid" | "minimal" | "square";
createdAt: Date;
updatedAt: Date;
};// Auto-handled by Better Auth
export const { GET, POST } = toNextJsHandler(auth.handler);Endpoints:
POST /api/auth/sign-in- Email/password sign inPOST /api/auth/sign-up- Email/password sign upGET /api/auth/session- Get current sessionPOST /api/auth/sign-out- Sign out userGET /api/auth/callback/github- GitHub OAuth callbackGET /api/auth/callback/google- Google OAuth callback
Direct file upload endpoint with authentication and validation.
// POST /api/upload
const formData = new FormData();
formData.append("file", file);
formData.append("projectId", "optional-project-id");
const response = await fetch("/api/upload", {
method: "POST",
body: formData,
});
const result = await response.json();
// Returns: { success: boolean, mediaId?: string, url?: string, error?: string }Features:
- File size limit: 20MB
- Supported formats: JPEG, PNG, WebP, GIF
- Automatic image optimization
- R2/S3 storage integration
- Project association
Generate presigned URLs for client-side uploads.
// POST /api/upload/presigned
const response = await fetch("/api/upload/presigned", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
fileName: "image.jpg",
fileType: "image/jpeg",
fileSize: 1024000,
}),
});
const { presignedUrl, fields } = await response.json();Media management endpoint.
// DELETE /api/media
const response = await fetch("/api/media", {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mediaId: "media-id" }),
});Update user profile with validation.
import { updateProfile } from "@/lib/actions/profile";
const result = await updateProfile({
profileData: {
title: "Senior Designer",
bio: "Passionate about creating beautiful digital experiences.",
location: "San Francisco, CA",
},
userData: {
name: "John Doe",
username: "johndoe",
email: "john@example.com",
},
socialLinks: [
{ platform: "twitter", url: "https://twitter.com/johndoe" },
{ platform: "linkedin", url: "https://linkedin.com/in/johndoe" },
],
profileImageFormData: formData, // Optional
});
if (result.success) {
console.log("Profile updated:", result.data);
} else {
console.error("Error:", result.error);
}Create a new profile for authenticated user.
import { createProfile } from "@/lib/actions/profile";
const result = await createProfile({
profileData: {
title: "Designer",
bio: "Hello world!",
location: "New York",
},
username: "newusername", // Optional username change
});Update username with validation and availability check.
import { updateUsername } from "@/lib/actions/profile";
const result = await updateUsername("newusername");Create a new project with automatic slug generation.
import { createProject } from "@/lib/actions/project";
const result = await createProject({
title: "My Awesome Project",
about: "A detailed description of my project.",
externalLink: "https://example.com",
imageIds: ["media-id-1", "media-id-2"],
featuredImageId: "media-id-1",
});
if (result.success) {
console.log("Project created:", result.data);
// { id: 'project-id', slug: 'my-awesome-project', title: 'My Awesome Project' }
}Update an existing project with ownership validation.
import { updateProject } from "@/lib/actions/project";
const result = await updateProject("project-id", {
title: "Updated Project Title",
about: "Updated description",
});Delete a project and associated media.
import { deleteProject } from "@/lib/actions/project";
const result = await deleteProject("project-id");Reorder projects by providing array of project IDs in desired order.
import { updateProjectOrder } from "@/lib/actions/project";
const result = await updateProjectOrder([
"project-id-3",
"project-id-1",
"project-id-2",
]);Upload image with automatic optimization and validation.
import { uploadImage } from "@/lib/actions/media";
const formData = new FormData();
formData.append("file", file);
const result = await uploadImage(formData, "optional-project-id");
if (result.success) {
console.log("Image uploaded:", result.mediaId);
}Generate AI description for images using Groq.
import { generateDescription } from "@/lib/actions/ai";
const result = await generateDescription("https://example.com/image.jpg");
if (result.success) {
console.log("Generated description:", result.data);
}Get current user session.
import { getSession } from "@/lib/actions/auth";
const session = await getSession();
if (session) {
console.log("User:", session.user);
}Sign out current user.
import { signOut } from "@/lib/actions/auth";
await signOut(); // Redirects to sign-in pageDelete user account and all associated data.
import { deleteAccount } from "@/lib/actions/auth";
const result = await deleteAccount();Flexible button component with variants.
import { Button } from '@/components/ui/button';
<Button variant="default" size="md">
Click me
</Button>
<Button variant="outline" size="sm">
Secondary
</Button>
<Button variant="destructive" size="lg">
Delete
</Button>Props:
variant: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'size: 'default' | 'sm' | 'lg' | 'icon'
Form input component with validation states.
import { Input } from '@/components/ui/input';
<Input
type="text"
placeholder="Enter your name"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<Input
type="email"
placeholder="Email"
error="Invalid email"
/>Drag-and-drop file upload component.
import {
FileUploader,
FileUploaderContent,
FileUploaderItem,
} from "@/components/ui/file-upload";
<FileUploader
value={files}
onValueChange={setFiles}
dropzoneOptions={{
accept: { "image/*": [".jpg", ".jpeg", ".png", ".webp"] },
maxSize: 20 * 1024 * 1024, // 20MB
}}
>
<FileUploaderContent>
{files.map((file, index) => (
<FileUploaderItem key={index} index={index}>
{file.name}
</FileUploaderItem>
))}
</FileUploaderContent>
</FileUploader>;Optimized image component with loading states.
import { AsyncImage } from "@/components/ui/async-image";
<AsyncImage
src="/path/to/image.jpg"
alt="Description"
width={400}
height={300}
className="rounded-lg"
/>;Main portfolio grid component with multiple layout options.
import { PortfolioGrid } from "@/components/profile/portfolio-grid";
<PortfolioGrid projects={projects} username="johndoe" gridType="masonry" />;Grid Types:
masonry- Pinterest-style variable heightsgrid- Standard uniform gridminimal- Clean minimal layoutsquare- Instagram-style square grid
Lead generation contact form.
import { ContactForm } from "@/components/profile/contact-form";
<ContactForm userId="user-id" portfolioOwner="John Doe" />;User profile header with avatar and basic info.
import { ProfileHeader } from "@/components/profile/profile-header";
<ProfileHeader username="johndoe" />;Complete project creation/editing form.
import { ProjectForm } from '@/components/admin/project-form';
<ProjectForm
mode="create"
onSuccess={(project) => console.log('Created:', project)}
onCancel={() => setShowForm(false)}
/>
<ProjectForm
mode="edit"
project={existingProject}
onSuccess={(project) => console.log('Updated:', project)}
/>Profile editing form with image upload.
import { ProfileForm } from "@/components/admin/profile-form";
<ProfileForm
user={user}
profile={profile}
onSuccess={() => console.log("Profile updated")}
/>;Theme customization form.
import { ThemeForm } from "@/components/admin/theme-form";
<ThemeForm user={user} theme={theme} />;Complete sign-in form with validation.
import SignInForm from "@/components/auth/sign-in-form";
<SignInForm />;Complete sign-up form with validation.
import SignUpForm from "@/components/auth/sign-up-form";
<SignUpForm />;OAuth login buttons for GitHub and Google.
import SocialLoginButtons from "@/components/auth/social-login-buttons";
<SocialLoginButtons
mode="signin" // or "signup"
onSuccess={() => console.log("Logged in")}
/>;Sign-up form state management with validation.
import { useSignUpForm } from "@/hooks/use-sign-up-form";
function SignUpPage() {
const { form, isLoading, passwordStrength, handleSubmit } = useSignUpForm();
return (
<form onSubmit={handleSubmit}>
<input {...form.register("name")} />
<input {...form.register("email")} />
<input {...form.register("password")} />
<input {...form.register("username")} />
<button disabled={isLoading}>Sign Up</button>
</form>
);
}Sign-in form state management.
import { useSignInForm } from "@/hooks/use-sign-in-form";
function SignInPage() {
const { form, isLoading, handleSubmit } = useSignInForm();
return (
<form onSubmit={handleSubmit}>
<input {...form.register("email")} />
<input {...form.register("password")} />
<button disabled={isLoading}>Sign In</button>
</form>
);
}Profile form with optimistic updates.
import { useProfileForm } from "@/hooks/use-profile-form";
function ProfileEditPage() {
const {
form,
isLoading,
socialLinks,
handleSubmit,
addSocialLink,
removeSocialLink,
} = useProfileForm(initialProfile, (data) => {
console.log("Profile saved:", data);
});
return <form onSubmit={handleSubmit}>{/* Form fields */}</form>;
}Check username availability in real-time.
import { useUsernameAvailability } from "@/hooks/use-username-availability";
function UsernameField() {
const [username, setUsername] = useState("");
const { isAvailable, isLoading, error } = useUsernameAvailability(username);
return (
<div>
<input value={username} onChange={(e) => setUsername(e.target.value)} />
{isLoading && <span>Checking...</span>}
{!isLoading && username && (
<span>{isAvailable ? "✓ Available" : "✗ Taken"}</span>
)}
</div>
);
}Password strength calculation.
import { usePasswordStrength } from "@/hooks/use-password-strength";
function PasswordField() {
const [password, setPassword] = useState("");
const { score, feedback, isValid } = usePasswordStrength(password);
return (
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<div>Strength: {score}/4</div>
<div>{feedback}</div>
</div>
);
}Mobile device detection.
import { useIsMobile } from "@/hooks/use-mobile";
function ResponsiveComponent() {
const isMobile = useIsMobile();
return <div>{isMobile ? <MobileLayout /> : <DesktopLayout />}</div>;
}Clean and validate username format.
import { cleanUsername } from "@/lib/utils/username";
const clean = cleanUsername("John Doe!"); // Returns: 'johndoe'Generate username from email address.
import { generateUsernameFromEmail } from "@/lib/utils/username";
const username = generateUsernameFromEmail("john.doe@example.com"); // Returns: 'johndoe'Generate username from display name.
import { generateUsernameFromName } from "@/lib/utils/username";
const username = generateUsernameFromName("John Doe"); // Returns: 'johndoe'Detect image type from URL or file extension.
import { detectImageType } from "@/lib/utils/media";
const type = detectImageType("image.gif"); // Returns: 'gif'Check if image is animated (GIF/WebP).
import { isAnimatedImage } from "@/lib/utils/media";
const isAnimated = isAnimatedImage("animation.gif"); // Returns: trueGet optimized loading settings for images.
import { getImageLoadingSettings } from "@/lib/utils/media";
const settings = getImageLoadingSettings("large-image.jpg");
// Returns: { loading: 'lazy', priority: false, quality: 85 }Check if user has active subscription.
import { hasActiveSubscription } from "@/lib/utils/subscription";
const isActive = hasActiveSubscription(user);Format subscription renewal date for display.
import { formatRenewalDate } from "@/lib/utils/subscription";
const formatted = formatRenewalDate(renewalDate); // Returns: 'December 25, 2024'Compress image file for optimization.
import { compressImage } from "@/lib/utils/image-compression";
const compressedFile = await compressImage(originalFile, {
maxSize: 1024 * 1024, // 1MB
quality: 0.8,
});Get user by ID with caching.
import { getUserById } from "@/lib/data/user";
const user = await getUserById("user-id");Get user by username with caching.
import { getUserByUsername } from "@/lib/data/user";
const user = await getUserByUsername("johndoe");Get user by custom domain.
import { getUserByCustomDomain } from "@/lib/data/user";
const user = await getUserByCustomDomain("johndoe.com");Get all projects for a user with caching.
import { getProjectsByUsername } from "@/lib/data/project";
const projects = await getProjectsByUsername("johndoe");Get specific project by username and slug.
import { getProjectByUsernameAndSlug } from "@/lib/data/project";
const project = await getProjectByUsernameAndSlug("johndoe", "my-project");Get total project count for user.
import { getProjectCount } from "@/lib/data/project";
const count = await getProjectCount("user-id");Get profile by username with social links.
import { getProfileByUsername } from "@/lib/data/profile";
const profile = await getProfileByUsername("johndoe");Get profile by user ID.
import { getProfileByUserId } from "@/lib/data/profile";
const profile = await getProfileByUserId("user-id");Get media item by ID.
import { getMediaById } from "@/lib/data/media";
const media = await getMediaById("media-id");Get all media for a project.
import { getMediaByProjectId } from "@/lib/data/media";
const mediaList = await getMediaByProjectId("project-id");// Extended project type with media
interface ProjectWithMedia extends Project {
featuredMedia?: Media;
additionalMedia?: Media[];
}
// User type from schema
type User = typeof user.$inferSelect;
// Project type from schema
type Project = typeof project.$inferSelect;
// Profile type from schema
type Profile = typeof profile.$inferSelect;
// Media type from schema
type Media = typeof media.$inferSelect;
// Social link type from schema
type SocialLink = typeof socialLink.$inferSelect;// Sign up validation
const signUpSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
username: z
.string()
.min(3)
.max(20)
.regex(/^[a-zA-Z0-9_-]+$/),
});
// Project creation validation
const createProjectSchema = z.object({
title: z.string().min(1).max(100),
slug: z
.string()
.min(1)
.max(100)
.regex(/^[a-z0-9-]+$/)
.optional(),
about: z.string().max(1000).optional(),
externalLink: z.string().url().optional().or(z.literal("")),
imageIds: z.array(z.string()).optional(),
featuredImageId: z.string().optional(),
});
// Profile update validation
const updateProfileSchema = z.object({
name: z.string().min(1).optional(),
title: z.string().max(100).optional(),
bio: z.string().max(500).optional(),
username: z
.string()
.min(3)
.max(20)
.regex(/^[a-zA-Z0-9_-]+$/)
.optional(),
socialLinks: z
.array(
z.object({
platform: z.string().min(1),
url: z.string().url(),
displayOrder: z.number().int().min(0),
})
)
.optional(),
});// Standard API response format
type ActionResponse<T> = {
success: boolean;
data?: T;
error?: string;
};
// Upload response
type UploadResponse = {
success: boolean;
mediaId?: string;
url?: string;
error?: string;
};
// Password strength response
type PasswordStrength = {
score: number; // 0-4
feedback: string;
isValid: boolean;
};# Database
DATABASE_URL=postgresql://...
# Authentication
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your-secret-key
# OAuth Providers
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# File Storage (R2/S3)
R2_ACCOUNT_ID=your-r2-account-id
R2_ACCESS_KEY_ID=your-r2-access-key
R2_SECRET_ACCESS_KEY=your-r2-secret-key
R2_BUCKET_NAME=your-bucket-name
R2_PUBLIC_URL=https://your-r2-domain.com
# Payments (Polar)
POLAR_ACCESS_TOKEN=your-polar-access-token
POLAR_WEBHOOK_SECRET=your-polar-webhook-secret
# AI (Groq)
GROQ_API_KEY=your-groq-api-key
# Analytics
NEXT_PUBLIC_POSTHOG_KEY=your-posthog-key
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
# Discord Notifications (Optional)
DISCORD_WEBHOOK_URL=your-discord-webhook-url
# Vercel (For custom domains)
VERCEL_ACCESS_TOKEN=your-vercel-token
VERCEL_TEAM_ID=your-team-idAll server actions and API routes follow a consistent error handling pattern:
// Success response
{
success: true,
data: { /* result data */ }
}
// Error response
{
success: false,
error: "Human-readable error message"
}Common error scenarios:
- Unauthorized: User not authenticated
- Validation Error: Invalid input data
- Not Found: Resource doesn't exist
- Conflict: Duplicate username/slug
- Rate Limited: Too many requests
- Server Error: Internal server error
- Authentication: Always check session before modifying data
- Validation: Use Zod schemas for all input validation
- Error Handling: Provide meaningful error messages
- Caching: Use cached data functions for read operations
- Revalidation: Revalidate paths after data mutations
- File Uploads: Validate file types and sizes
- Performance: Use lazy loading for images
- Accessibility: Include alt text for images
- SEO: Generate proper meta tags for public pages
- Security: Sanitize user input and validate ownership
This documentation covers all major APIs, functions, and components in the Wrk.so platform. For specific implementation details, refer to the individual source files in the codebase.