How to Set Up a Supabase Credits System for awesome-gpt-image-2: Database Functions, RLS, and API Integration
To set up a Supabase credits system for AI image generation, you implement PostgreSQL tables for user profiles and transactions, row-level security policies, and atomic database functions that handle credit consumption, free generation quotas, and automatic refunds when operations fail.
Setting up a Supabase credits system requires architecting database tables that track balances, transactions, and pending operations while enforcing security through row-level security (RLS). The awesome-gpt-image-2 project demonstrates this implementation using PostgreSQL functions to handle free generation quotas, credit consumption, and automatic refunds. By following the schema defined in supabase/migrations/202605090001_user_credits.sql and the client configuration in src/supabaseClient.js, developers can deploy a tamper-proof credit economy for AI services.
Database Schema Architecture
The profiles Table
Located in supabase/migrations/202605090001_user_credits.sql, the profiles table stores the core user state. The credit_balance column tracks purchasable credits available for image generation, while free_generations_used counts consumed complimentary generations. Each row represents a single user's current financial and quota state within the system.
The credit_transactions Table
The credit_transactions table provides an immutable ledger of every credit modification. It records grants, purchases, generation consumption, refunds, and manual adjustments. This audit trail enables administrators to trace the complete history of any credit balance change while supporting data analysis for business dashboards.
The generation_reservations Table
Acting as a temporary holding pattern for active requests, the generation_reservations table stores case IDs, prompts, consumption amounts, and status values (pending, succeeded, failed). This table bridges the gap between credit deduction and successful image delivery, ensuring that credits are only permanently consumed after confirmed generation completion.
Row-Level Security Implementation
RLS policies defined in the migration file restrict data access so users can only interact with their own rows. The policies named "Users can read own …" apply to each table, preventing unauthorized balance queries or transaction history access. Combined with PostgreSQL's security model, these policies guarantee that credit operations cannot be tampered with from the client side.
Core Database Functions
reserve_generation_usage
The reserve_generation_usage function serves as the primary entry point for consumption attempts. When called via RPC from api/generate-image.js, it performs the following atomic operations:
- Locks the caller's profile row to prevent race conditions
- Checks if the user has remaining free generations; if so, increments
free_generations_usedand creates a reservation withused_free_generation = true - If no free generation remains, verifies
credit_balance >= 1, deducts one credit, creates a reservation, and logs a "generation" transaction incredit_transactions - Throws a
CREDITS_REQUIREDerror if neither condition is met
This function returns the reservation ID, generation type used, and current balance state.
complete_generation_reservation and release_generation_reservation
Once the image generation pipeline finishes successfully, the worker calls complete_generation_reservation with the p_reservation_id parameter. This function updates the reservation status to succeeded and sets the completed_at timestamp, finalizing the credit consumption.
When generation fails, the release_generation_reservation function handles automatic rollback. If the reservation consumed a free generation, the function decrements free_generations_used to restore the quota. If a credit was spent, the function refunds the credit_balance and creates a "refund" transaction entry in credit_transactions, maintaining accurate financial records while preserving user funds.
Client Configuration and API Layer
Supabase Client Initialization
The front-end and API routes rely on src/supabaseClient.js to instantiate the Supabase client. This wrapper loads the VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables, exporting both a standard supabase instance for user operations and configuration checks.
import { supabase, isSupabaseConfigured } from '@/supabaseClient';
if (!isSupabaseConfigured) {
console.error('Supabase isn’t configured – check VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY.');
}
Image Generation Endpoint
The api/generate-image.js route orchestrates the credit reservation and generation trigger. It invokes client.rpc('reserve_generation_usage', { p_user_id, p_case_id, p_prompt }) to atomically reserve credits before processing the image request. If the RPC returns a CREDITS_REQUIRED error, the endpoint immediately returns an insufficient credits response to the client.
async function reserveGeneration(caseId, prompt) {
const { data, error } = await supabase.rpc(
'reserve_generation_usage',
{
p_user_id: supabase.auth.user().id,
p_case_id: caseId,
p_prompt: prompt,
}
);
if (error) {
if (error.message.includes('CREDITS_REQUIRED')) {
alert('You need more credits to generate images.');
} else {
console.error(error);
}
return null;
}
// data = { reservation_id, used_free_generation, credit_amount, free_generations_used, credit_balance }
return data;
}
Admin Credit Management
Administrators can adjust user balances through api/admin/credits/adjust.js, which utilizes a service-role client from api/_lib/supabase.js. This endpoint bypasses RLS to directly modify profiles.credit_balance and append corresponding entries to credit_transactions with types "grant" or "adjustment".
// POST /api/admin/credits/adjust
// Body: { userId: "...", amount: 10, reason: "Promotional grant" }
import { supabaseAdmin } from '@/supabaseClient'; // admin-role client
export default async function handler(req, res) {
const { userId, amount, reason } = req.body;
// Update balance
await supabaseAdmin
.from('profiles')
.update({ credit_balance: supabaseAdmin.raw('credit_balance + ?', [amount]) })
.eq('id', userId);
// Log transaction
await supabaseAdmin.from('credit_transactions').insert({
user_id: userId,
amount,
type: amount > 0 ? 'grant' : 'adjustment',
source: 'admin_adjust',
metadata: { reason },
});
res.status(200).json({ ok: true });
}
Worker Completion Flow
After the background generation service completes or fails, it interacts with the reservation system to finalize state.
Successful completion uses complete_generation_reservation:
// In the worker after the image is generated
await supabase.rpc('complete_generation_reservation', {
p_reservation_id: reservationId,
});
Failed generation triggers release_generation_reservation:
await supabase.rpc('release_generation_reservation', {
p_reservation_id: reservationId,
p_error_code: 'IMAGE_RENDER_TIMEOUT',
});
Summary
- Atomic transactions in PostgreSQL functions prevent race conditions when deducting credits or consuming free generations
- Three core tables (
profiles,credit_transactions,generation_reservations) track balances, audit trails, and pending operations - Row-level security policies ensure users can only access their own credit data
- Automatic rollback through
release_generation_reservationrefunds credits or restores free generations when image generation fails - Service-role clients in
api/_lib/supabase.jsenable privileged admin operations while maintaining security boundaries
Frequently Asked Questions
How does the system handle concurrent generation requests?
The reserve_generation_usage function employs row-level locking when selecting the user's profile, ensuring that simultaneous requests from the same user cannot double-spend credits or free generations. PostgreSQL's atomic transaction guarantees that only one reservation succeeds per available credit.
What happens if an image fails to generate after credits are deducted?
The system uses the release_generation_reservation function to automatically refund consumed resources. If a credit was spent, the function increments credit_balance and creates a "refund" transaction; if a free generation was used, it decrements free_generations_used to restore the user's quota.
Can users manipulate their credit balances through the front-end?
No. All credit modifications occur inside server-side database functions protected by RLS policies. The credit_transactions table is append-only from the user's perspective, and balance updates happen exclusively through the reserve_generation_usage, release_generation_reservation, and admin adjustment functions, preventing client-side tampering.
How do I configure the Supabase client for local development?
Set the VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY environment variables in your .env file. The src/supabaseClient.js file automatically loads these values and exports a configured client instance plus an isSupabaseConfigured boolean for validation checks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →