API Reference
This document provides comprehensive reference for TinyKit SaaS's API patterns, function conventions, and development standards.
API Design Patterns
Function Organization
TinyKit SaaS follows a consistent domain-based organization pattern:
convex/
├── domain/
│ ├── public/ # Public API functions
│ │ ├── queries.ts # Read operations (SELECT)
│ │ ├── mutations.ts # Write operations (INSERT, UPDATE, DELETE)
│ │ └── actions.ts # External API calls (Stripe, Resend, etc.)
│ ├── internal/ # Internal helper functions
│ ├── helpers.ts # Domain utilities and permission checks
│ └── schema.ts # Domain type definitionsFunction Naming Conventions
Admin Functions (Suffix Pattern)
All admin functions follow the *Admin suffix pattern for clear identification:
// Queries
getAllNotificationsAdmin; // Get paginated notification history
getNotificationStatsAdmin; // System-wide statistics
searchUsersAdmin; // User search for admin interface
searchTeamsAdmin; // Team search for targeting
// Mutations
sendNotificationToAllUsersAdmin; // Broadcast to all users
sendNotificationToAllTeamOwnersAdmin; // Broadcast to team owners
bulkMarkAsReadAdmin; // Bulk operations
createTeamAdmin; // Admin-created teamsPermission-Based Function Names
// Role requirements implied by naming
requireAdmin* // Admin only functions
requireTeamOwner* // Team owner functions
requireTeamAdmin* // Team admin/owner functionsType Safety Patterns
Convex Validators
Always use Convex validators for runtime type checking:
import { v } from "convex/values";
export const createTeam = mutation({
args: {
name: v.string(),
description: v.optional(v.string()),
initialMembers: v.array(v.string()),
},
handler: async (ctx, args) => {
// Runtime validation ensures args match exactly
},
});Generated Types
Use Convex generated types for complete type safety:
import type { Id } from "@/convex/_generated/dataModel";
import { api } from "@/convex/_generated/api";
// Use generated ID types
const teamId: Id<"teams"> = "team123" as Id<"teams">;
// Use generated API for function calls
const team = useQuery(api.teams.queries.getById, { id: teamId });Authentication & Authorization
Core Access Control Functions
Location: convex/lib/access/ (modular policy-based access control with 7 specialized files)
// Import from modular access control system
import {
requireAccess,
hasAccess,
requireAccessForAction,
getUserRolePermissions,
getOrgRolePermissions,
} from "@/convex/lib/access";
// Primary access control function - enforces requirements (requireAccess.ts)
export async function requireAccess(
ctx: QueryCtx | MutationCtx,
options: HasAccessOptions,
): Promise<AccessContext>;
// Conditional access checking - returns boolean (requireAccess.ts)
export async function hasAccess(
ctx: QueryCtx | MutationCtx,
options: HasAccessOptions,
): Promise<boolean>;
// Action-specific access control (requireAccessForAction.ts)
export async function requireAccessForAction(
ctx: ActionCtx,
options: HasAccessOptions,
): Promise<boolean>;
// Permission array generation utilities (utils.ts)
export function getUserRolePermissions(userRole: string | null): string[];
export function getOrgRolePermissions(orgRole: string | null): string[];
// Access options interface (types.ts) - supports arrays for multiple allowed roles
interface HasAccessOptions {
userRole?: "admin" | "user" | Array<"admin" | "user">;
orgRole?: "owner" | "admin" | "member" | Array<"owner" | "admin" | "member">;
permission?: string; // "resource:action" format
minPersonalAccessLevel?: number; // 0-3 (database-driven from products table)
minOrgAccessLevel?: number; // 0-3 (database-driven from products table)
orgId?: Id<"orgs">;
condition?: (ctx: AccessContext) => boolean | Promise<boolean>;
}
// Access context with guaranteed non-null values (types.ts)
interface AccessContext {
userId: Id<"users">; // Guaranteed non-null
user: Doc<"users">; // Guaranteed non-null
userRole: string; // User's role
orgRole: string | null; // Org role if orgId provided
personalAccessLevel: number; // Personal access level (0-3, database-driven)
orgAccessLevel: number | null; // Org access level if orgId provided (database-driven)
}Permission Array System
Location: Backend generates permission arrays, frontend uses for performance optimization
// Backend: Generate permission arrays in user queries
import {
getUserRolePermissions,
getOrgRolePermissions,
} from "@/convex/lib/access";
export const me = query({
args: {},
handler: async (ctx) => {
const { userId, user, orgRole } = await requireAccess(ctx, {
userRole: ["user"],
});
// Generate permission arrays for frontend
const userPermissions = getUserRolePermissions(user.userRole || "user");
const orgPermissions = getOrgRolePermissions(orgRole);
return {
...user,
permissions: userPermissions, // ["users:read", "users:update", ...]
orgPermissions: orgPermissions, // ["orgMembers:invite", "messages:create", ...]
};
},
});
// Frontend: Fast permission checking using .includes()
const userData = useQuery(api.users.private.queries.getMe);
// Direct permission array checking (fastest)
const canDeleteUsers = userData?.permissions?.includes("users:delete") ?? false;
const canInviteMembers =
userData?.orgPermissions?.includes("orgMembers:invite") ?? false;Modular Architecture Files
Organization: Seven specialized files in convex/lib/access/
index.ts- Main exports and comprehensive documentationtypes.ts- Type definitions (HasAccessOptions, AccessContext)requireAccess.ts- Primary access enforcement functionrequireAccessForAction.ts- Action-specific access controlbuildAccessContext.ts- Context building logiccheckPermission.ts- Permission checking utilitiesutils.ts- Helper functions for permission arrays
Policy-Based Configuration
Location: convex/lib/permissions.ts (centralized policy configuration)
// User role permissions
export const userRolePermissions = {
admin: { all: true },
user: {
profile: ["read", "update"],
teams: ["create", "join"],
},
} as const;
// Team role permissions
export const teamRolePermissions = {
owner: { all: true },
admin: {
team: ["read", "update"],
members: ["read", "invite", "remove"],
content: ["create", "read", "update", "delete"],
},
member: {
team: ["read"],
content: ["create", "read"],
},
} as const;
// Database-driven access levels (stored in products table)
// Access levels are no longer hardcoded but retrieved from products.accessLevel field
// This provides a single source of truth for subscription-based access control
export const accessLevels = {
0: "Free", // Basic features
1: "Basic", // Essential premium features
2: "Pro", // Advanced features
3: "Enterprise", // All features
} as const;Access Control Implementation Patterns
Always use the policy-based access control system for protection:
export const protectedMutation = mutation({
args: {
teamId: v.id("teams"),
data: v.object({}),
},
handler: async (ctx, args) => {
// 1. Enforce access requirements FIRST (using modular system)
const { userId, teamRole } = await requireAccess(ctx, {
teamId: args.teamId,
teamRole: "owner",
});
// 2. Guaranteed access - proceed with business logic
const team = await ctx.db.get(args.teamId);
if (!team) {
throw new ConvexError("Team not found");
}
// 3. Execute operation with context
return await ctx.db.patch(args.teamId, {
...args.data,
updatedBy: userId,
updatedAt: Date.now(),
});
},
});
// Conditional access example
export const getTeamData = query({
args: { teamId: v.id("teams") },
handler: async (ctx, args) => {
// Check access without throwing
const canAccess = await hasAccess(ctx, {
teamId: args.teamId,
teamRole: "member",
});
if (!canAccess) {
return { error: "Team access required" };
}
return await ctx.db.get(args.teamId);
},
});
// Complex access control example
export const upgradeTeamPlan = mutation({
args: { teamId: v.id("teams"), planId: v.string() },
handler: async (ctx, args) => {
// Multiple access requirements in single call
const { userId, orgRole } = await requireAccess(ctx, {
orgId: args.orgId,
orgRole: "owner", // Must be org owner
minOrgAccessLevel: 1, // Must have paid subscription
});
// All requirements verified - proceed
return await upgradeSubscription(args.teamId, args.planId);
},
});Utility Helper Functions
Billing & Subscription Helpers
The billing system provides essential helper functions for subscription management and access control:
Subscription Status Checking
// Enhanced subscription activity check with expiration handling
export function isSubscriptionActive(
subscription: Doc<"subscriptions">,
): boolean {
const isStatusActive =
subscription.status === "active" || subscription.status === "trialing";
// If status is not active, subscription is definitely not active
if (!isStatusActive) {
return false;
}
// If subscription is canceled at period end and has expired, it's not active
if (subscription.cancelAtPeriodEnd && hasExpired(subscription)) {
return false;
}
return true;
}Policy-Based Access Control System
Function Names and Purpose
Clear, Descriptive Function Names:
requireAccess()- Enforces access requirements, throws on failurerequireAccessForAction()- Enforces access in Convex actions- Frontend
hasAccess()- Checks access conditions, returns boolean
Core Access Control Functions
requireAccess() - Enforcement Function
Primary function for enforcing access control with guaranteed non-null results:
import { requireAccess } from "../../lib/access";
export const deleteUser = mutation({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
// Enforces access - throws descriptive error if denied
const { userId: adminId } = await requireAccess(ctx, {
userRole: "admin",
});
// Guaranteed access - proceed with operation
await ctx.db.delete(args.userId);
return { success: true };
},
});
// Complex access control example
const { userId, orgRole, personalAccessLevel } = await requireAccess(ctx, {
userRole: "user", // Must be authenticated
orgId: args.orgId, // Organization context
orgRole: "owner", // Must be org owner
minPersonalAccessLevel: 1, // Must have premium subscription
permission: "orgs:billing", // Must have billing permission
});hasAccess() - Conditional Function
Returns boolean for conditional logic without throwing:
import { hasAccess } from "../../lib/access";
export const getAdminStats = query({
args: {},
handler: async (ctx) => {
// Check access without throwing
const isAdmin = await hasAccess(ctx, {
userRole: "admin",
});
if (!isAdmin) {
return { message: "Admin access required" };
}
return await generateAdminStatistics(ctx);
},
});
// Multiple condition checks
const canManageOrg = await hasAccess(ctx, {
orgId: args.orgId,
orgRole: "owner",
});
const hasAdvancedFeatures = await hasAccess(ctx, {
minPersonalAccessLevel: 1, // Premium subscription
});requireAccessForAction() - Action Function
Specialized for Convex actions using internal queries:
import { requireAccessForAction } from "../../lib/access";
export const sendNotificationEmail = action({
args: { userId: v.id("users"), message: v.string() },
handler: async (ctx, args) => {
// Actions require special handling
const hasAccess = await requireAccessForAction(ctx, {
permission: "notifications:send",
});
if (!hasAccess) {
throw new Error("Insufficient permissions");
}
// Send email via Resend
return await sendEmailViaResend(args.message);
},
});Usage Patterns
// Check if organization has access to a feature
const { orgAccessLevel } = await requireAccess(ctx, {
orgId: args.orgId,
minOrgAccessLevel: 2, // Enterprise tier
});
// Verify subscription is still active before allowing operation
const subscription = await ctx.db
.query("subscriptions")
.withIndex("by_org", (q) => q.eq("orgId", args.orgId))
.first();
if (!subscription || !isSubscriptionActive(subscription)) {
throw new ConvexError("Active subscription required");
}Key Benefits
- Real-time Expiration: No waiting for webhooks to revoke access
- Graceful Cancellation: Users retain access until their paid period ends
- Backward Compatibility: Existing code automatically benefits from enhanced checks
- Consistent Logic: Same helper functions used across team and personal subscriptions
Rate Limiting API
requireRateLimit()
Protects mutations from abuse and prevents excessive resource usage.
Import:
import { requireRateLimit } from "../../lib/access";Function Signature:
async function requireRateLimit(
ctx: MutationCtx | QueryCtx | ActionCtx,
limitType: string,
options: {
key: string | Id<TableNames>;
count?: number;
throws?: boolean;
},
): Promise<{ ok: boolean; retryAfter?: number }>;Pre-configured Rate Limits:
authAttempts- 10/hour (fixed window) - Sign in, sign up, password resetprofileUpdates- 5/min, burst 10 (token bucket) - Profile changescontentCreation- 20/min, burst 30 (token bucket) - Posts, comments, messagesadminOperations- 100/hour, burst 150 (token bucket) - Admin actionsexternalApiCalls- 50/hour, burst 100 (token bucket) - Third-party APIsfileUploads- 10/hour, burst 20 (token bucket) - File storagesearchQueries- 30/min, burst 50 (token bucket) - Search operations
Usage Examples:
// Standard rate limiting
export const updateProfile = mutation({
args: { name: v.string() },
handler: async (ctx, args) => {
const { userId } = await requireAccess(ctx, { userRole: ["user"] });
await requireRateLimit(ctx, "profileUpdates", { key: userId });
await ctx.db.patch(userId, { name: args.name });
},
});
// Custom token consumption
export const processLargeRequest = mutation({
args: { data: v.string(), estimatedCost: v.number() },
handler: async (ctx, args) => {
const { userId } = await requireAccess(ctx, { userRole: ["user"] });
await requireRateLimit(ctx, "externalApiCalls", {
key: userId,
count: args.estimatedCost, // Consume multiple tokens
});
// Process request
},
});
// Non-throwing check
export const checkUploadAvailability = query({
args: {},
handler: async (ctx) => {
const { userId } = await requireAccess(ctx, { userRole: ["user"] });
const { ok, retryAfter } = await requireRateLimit(ctx, "fileUploads", {
key: userId,
throws: false,
});
return { canUpload: ok, retryAfter };
},
});When to Use:
- ✅ User-facing mutations (profile updates, content creation)
- ✅ Authentication operations (sign in, sign up, password reset)
- ✅ File uploads and expensive operations
- ✅ External API calls
- ✅ Admin operations (prevent accidental abuse)
- ❌ Internal mutations (already protected)
- ❌ Simple queries (unless computationally expensive)
Database Query Patterns
Efficient Query Design
Use Indexes for Performance
// Good: Use indexes for efficient queries
export const getTeamMessages = query({
args: { teamId: v.id("teams") },
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_team", (q) => q.eq("teamId", args.teamId))
.order("desc")
.take(50);
},
});
// Bad: Full table scan
export const getTeamMessagesSlow = query({
args: { teamId: v.id("teams") },
handler: async (ctx, args) => {
const allMessages = await ctx.db.query("messages").collect();
return allMessages.filter((m) => m.teamId === args.teamId);
},
});Pagination Patterns
// Cursor-based pagination for performance
export const getPaginatedMessages = query({
args: {
teamId: v.id("teams"),
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
return await ctx.db
.query("messages")
.withIndex("by_team", (q) => q.eq("teamId", args.teamId))
.order("desc")
.paginate(args.paginationOpts);
},
});Database Schema Patterns
Relationship Design
// One-to-many: Team to Members
teams: {
_id: Id<"teams">,
name: string,
slug: string,
// ... other fields
}
teamMembers: {
_id: Id<"teamMembers">,
teamId: Id<"teams">, // Foreign key to teams
userId: Id<"users">, // Foreign key to users
teamRole: "owner" | "admin" | "member",
joinedAt: number
}
// Indexes for efficient queries
teamMembers.byTeam: ["teamId"]
teamMembers.byUser: ["userId"]
teamMembers.byTeamAndUser: ["teamId", "userId"]Error Handling Patterns
Consistent Error Responses
export const safeMutation = mutation({
args: { data: v.object({}) },
handler: async (ctx, args) => {
try {
// Validate permissions using policy-based access control
const { userId } = await requireAccess(ctx, {
role: "admin",
});
// Execute operation
const result = await performOperation(ctx, args.data);
return { success: true, data: result };
} catch (error) {
// Log for debugging
logger.error("Operation failed:", error);
// Return user-friendly error
throw new ConvexError(
error instanceof ConvexError
? error.message
: "Operation failed. Please try again.",
);
}
},
});Frontend Error Handling
const MyComponent = () => {
const mutation = useMutation(api.domain.mutations.safeMutation);
const handleAction = async (data) => {
try {
const result = await mutation({ data });
if (result.success) {
toast.success("Operation completed successfully!");
return result.data;
}
} catch (error) {
toast.error(error.message || "Something went wrong");
logger.error("Action failed:", error);
}
};
return (
<Button onClick={() => handleAction(formData)}>
Execute Action
</Button>
);
};Real-time Subscription Patterns
Reactive Data Pattern
// Component automatically updates when data changes
const TeamDashboard = ({ teamSlug }: { teamSlug: string }) => {
// Real-time team data
const team = useQuery(api.teams.queries.getBySlug, { slug: teamSlug });
// Real-time messages
const messages = useQuery(
api.messages.queries.getTeamMessages,
team?._id ? { teamId: team._id } : "skip"
);
// Real-time member list
const members = useQuery(
api.teams.queries.getTeamMembers,
team?._id ? { teamId: team._id } : "skip"
);
if (!team) return <LoadingSkeleton />;
return (
<div>
<TeamHeader team={team} />
<MemberList members={members} />
<MessageList messages={messages} />
</div>
);
};Optimistic Updates
const useSendMessage = (teamId: Id<"teams">) => {
const sendMessage = useMutation(api.messages.mutations.send);
const [optimisticMessage, setOptimisticMessage] = useState<string | null>(
null,
);
const handleSend = async (content: string) => {
// Show optimistic update immediately
setOptimisticMessage(content);
try {
await sendMessage({ teamId, content });
// Success - optimistic update is replaced by real data
setOptimisticMessage(null);
} catch (error) {
// Error - rollback optimistic update
setOptimisticMessage(null);
throw error;
}
};
return { handleSend, optimisticMessage };
};External API Integration Patterns
Stripe Integration (Actions)
// Stripe operations use actions for external API calls
export const createCheckoutSession = action({
args: {
teamId: v.id("teams"),
priceId: v.string(),
successUrl: v.string(),
cancelUrl: v.string(),
},
handler: async (ctx, args) => {
// Get team and validate permissions
const team = await ctx.runQuery(api.teams.queries.getById, {
id: args.teamId,
});
if (!team) {
throw new ConvexError("Team not found");
}
// Check user permissions using policy-based access control
const { userId } = await requireAccess(ctx, {
teamId: args.teamId,
teamRole: "owner",
});
// Create Stripe checkout session
const session = await stripe.checkout.sessions.create({
customer: team.stripeCustomerId,
payment_method_types: ["card"],
line_items: [
{
price: args.priceId,
quantity: 1,
},
],
mode: "subscription",
success_url: args.successUrl,
cancel_url: args.cancelUrl,
});
return { sessionId: session.id, url: session.url };
},
});Email Integration (Actions)
// Email sending uses actions for external API calls
export const sendNotificationEmail = action({
args: {
to: v.string(),
template: v.string(),
data: v.object({}),
},
handler: async (ctx, args) => {
// Render email template
const emailHtml = await renderEmailTemplate(args.template, args.data);
// Send via Resend
const result = await resend.sendEmail(ctx, {
from: "TinyKit SaaS <noreply@example.com>",
to: args.to,
subject: args.data.subject,
html: emailHtml,
});
// Update database with delivery status
await ctx.runMutation(api.notifications.mutations.updateEmailStatus, {
notificationId: args.data.notificationId,
emailSent: true,
emailSentAt: Date.now(),
});
return result;
},
});Testing Patterns
Function Testing
// Test Convex functions with proper setup
describe("Team Management", () => {
let convex: ConvexTestingHelper;
beforeEach(async () => {
convex = new ConvexTestingHelper();
await convex.mutation(api.teams.mutations.create, {
name: "Test Team",
description: "A test team",
});
});
it("should allow team owner to update settings", async () => {
const teamId = await convex.mutation(api.teams.mutations.create, {
name: "Test Team",
});
const result = await convex.mutation(api.teams.mutations.update, {
teamId,
updates: { name: "Updated Team" },
});
expect(result.success).toBe(true);
});
it("should deny non-owners from updating settings", async () => {
// Test permission denial
await expect(
convex.mutation(api.teams.mutations.update, {
teamId: "unauthorized-team",
updates: { name: "Hacked Team" },
}),
).rejects.toThrow("Insufficient permissions");
});
});Performance Optimization Patterns
Query Optimization
// Batch related queries efficiently
export const getTeamDashboardData = query({
args: { teamId: v.id("teams") },
handler: async (ctx, args) => {
// Check permissions once using policy-based access control
const canAccess = await hasAccess(ctx, {
teamId: args.teamId,
teamRole: "member",
});
if (!canAccess) {
return null;
}
// Batch all required data
const [team, members, recentMessages, stats] = await Promise.all([
ctx.db.get(args.teamId),
ctx.db
.query("teamMembers")
.withIndex("by_team", (q) => q.eq("teamId", args.teamId))
.collect(),
ctx.db
.query("messages")
.withIndex("by_team", (q) => q.eq("teamId", args.teamId))
.order("desc")
.take(10),
getTeamStats(ctx, args.teamId),
]);
return { team, members, recentMessages, stats };
},
});Frontend Performance
// Memoized components for expensive renders
const TeamMemberList = React.memo(({ members }: { members: TeamMember[] }) => {
const sortedMembers = useMemo(() => {
// Sort by role: owners first, then admins, then members
const roleOrder = { owner: 0, admin: 1, member: 2 };
return members.sort((a, b) =>
roleOrder[a.teamRole as keyof typeof roleOrder] -
roleOrder[b.teamRole as keyof typeof roleOrder]
);
}, [members]);
return (
<div>
{sortedMembers.map(member => (
<MemberCard key={member._id} member={member} />
))}
</div>
);
});Security Patterns
Input Validation
// Server-side validation with Convex validators
export const createUser = mutation({
args: {
email: v.string(),
firstName: v.string(),
lastName: v.string(),
userRole: v.union(v.literal("user"), v.literal("admin")),
},
handler: async (ctx, args) => {
// Additional business logic validation
if (!args.email.includes("@")) {
throw new ConvexError("Invalid email address");
}
if (args.firstName.length < 1 || args.lastName.length < 1) {
throw new ConvexError("Name fields are required");
}
// Permission check for admin role assignment
if (args.userRole === "admin") {
await requireAccess(ctx, {
userRole: "admin",
});
}
// Execute with validated data
return await ctx.db.insert("users", args);
},
});Data Sanitization
// Sanitize user input for security
export const createAnnouncement = mutation({
args: {
title: v.string(),
content: v.string(),
},
handler: async (ctx, args) => {
// Sanitize inputs
const sanitizedTitle = args.title.trim().slice(0, 100);
const sanitizedContent = args.content.trim().slice(0, 1000);
// Validate sanitized inputs
if (!sanitizedTitle || !sanitizedContent) {
throw new ConvexError("Title and content are required");
}
return await ctx.db.insert("announcements", {
title: sanitizedTitle,
content: sanitizedContent,
createdAt: Date.now(),
});
},
});User Profile Helper Functions
Picture URL Resolution
Location: convex/users/helpers.ts
The getUserPictureUrl helper provides consistent picture URL resolution with OAuth provider image support:
// Get user picture URL with proper fallback handling
export async function getUserPictureUrl(
user: { pictureStorageId?: string; image?: string },
ctx: { storage: { getUrl: (id: string) => Promise<string | null> } },
): Promise<string | null>;Usage in Queries
import { getUserPictureUrl } from "../helpers";
export const getUser = query({
args: { userId: v.id("users") },
handler: async (ctx, { userId }) => {
const user = await ctx.db.get(userId);
if (!user) return null;
// Handles both uploaded and OAuth images automatically
const pictureUrl = await getUserPictureUrl(user, ctx);
return {
...user,
pictureUrl,
};
},
});Image Priority System
- User-uploaded images (
pictureStorageId) - Takes priority when users upload custom avatars - OAuth provider images (
imagefield) - GitHub, Google, Apple profile pictures - Default state (
null) - No image available
Implementation Benefits
- OAuth Integration: Automatically displays GitHub, Google, and Apple profile pictures
- Seamless Upgrades: User uploads override OAuth images without breaking functionality
- Consistent API: All components use the same
pictureUrlfield regardless of image source - Performance Optimized: Single helper function used across all user queries
- Type Safe: Proper TypeScript types for user objects and context
Next.js Configuration
OAuth profile images require Next.js image domain configuration:
// next.config.ts
images: {
remotePatterns: [
{
protocol: "https",
hostname: "avatars.githubusercontent.com", // GitHub
pathname: "/**",
},
{
protocol: "https",
hostname: "lh3.googleusercontent.com", // Google
pathname: "/**",
},
{
protocol: "https",
hostname: "appleid.cdn-apple.com", // Apple
pathname: "/**",
},
],
},Project Architecture
TinyKit SaaS is built with a modern, scalable architecture using cutting-edge technologies for real-time collaboration and organization productivity.
Deployment Guide
This guide covers deploying TinyKit SaaS to production environments, including Vercel deployment, environment configuration, and production best practices.