Credits & Referral System
User credits and referral program for ProductReady
Credits & Referral System
ProductReady includes a built-in credits system and referral program to incentivize user growth and engagement.
Credits System
Credits are a virtual currency that users can earn and use within the platform. Credits are stored in cents (e.g., 1000 = $10.00).
Credit Sources
Users can earn credits through various activities:
| Source | Description |
|---|---|
referral | Inviting friends to sign up |
onboarding | Completing onboarding steps |
early_adopter | Early adopter rewards |
promotion | Various promotional campaigns |
manual | Admin-granted credits |
redemption | Redeeming promotional codes |
Database Schema
// User credits balance
export const userCredits = pgTable("user_credits", {
userId: text("user_id").primaryKey(),
balance: integer("balance").default(0).notNull(),
totalEarned: integer("total_earned").default(0).notNull(),
totalUsed: integer("total_used").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at"),
});
// Transaction history
export const userCreditTransactions = pgTable("user_credit_transactions", {
id: text("id").primaryKey(),
userId: text("user_id").notNull(),
amount: integer("amount").notNull(),
balanceAfter: integer("balance_after").notNull(),
source: text("source").notNull(),
referenceId: text("reference_id"),
description: text("description"),
grantedBy: text("granted_by"),
spaceId: text("space_id"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});API Endpoints
Get My Credits
// Get current user's credit balance
const credits = await trpc.credits.getMyCredits.query();
// Returns: { userId, balance, totalEarned, totalUsed, balanceFormatted }Get My Transactions
// Get transaction history
const { transactions, total } = await trpc.credits.getMyTransactions.query({
limit: 20,
offset: 0,
source: "referral", // optional filter
});Admin Endpoints
System admins can manage user credits:
// Add credits to a user
await trpc.credits.addCredits.mutate({
userId: "user_123",
amount: 500, // $5.00
source: "manual",
description: "Customer support credit",
});
// List all transactions
const { transactions, total } = await trpc.credits.listTransactions.query({
userId: "user_123", // optional
limit: 50,
});
// List users with credits
const { users, total } = await trpc.credits.listUsersWithCredits.query({
limit: 20,
});Referral Program
The referral program allows users to invite friends and earn credits when they sign up.
How It Works
- User generates a unique referral code
- User shares their referral link:
https://yourapp.com/sign-up?ref=CODE - New user signs up using the link
- Both users receive credits:
- Referrer: $5.00 in credits
- New User: $2.00 in credits
Database Schema
Referral fields are added to user_profiles:
export const userProfiles = pgTable("user_profiles", {
// ... other fields
referralCode: text("referral_code").unique(),
referredBy: text("referred_by"),
referralRewarded: timestamp("referral_rewarded"),
});API Endpoints
Get Referral Info
const info = await trpc.referral.getMyReferralInfo.query();
// Returns: { referralCode, referralCount, totalEarned }Generate Referral Code
const { referralCode } = await trpc.referral.generateReferralCode.mutate();Validate Referral Code (Public)
const { valid, referrerName } = await trpc.referral.validateReferralCode.query({
code: "ABC123",
});UI Components
The credits and referral UI is integrated into the Account Settings modal:
import { AccountSettingsModal } from "~/components/dashboard/account-settings-modal";
// The modal includes tabs for:
// - Profile
// - Credits (balance + transactions)
// - Referral (link + stats)
// - API KeysSign-Up Flow
The sign-up page automatically handles referral codes:
- Reads
?ref=CODEfrom URL - Validates the code and shows referrer name
- Stores code in sessionStorage for OAuth flow
- After registration, processes referral in dashboard
// In sign-up page
const refCode = searchParams.get("ref");
const { data: refValidation } = trpc.referral.validateReferralCode.useQuery(
{ code: refCode || "" },
{ enabled: !!refCode }
);Customization
You can customize reward amounts in src/server/routers/referral.ts:
const REFERRER_REWARD = 500; // $5.00 for the referrer
const REFERRED_REWARD = 200; // $2.00 for the new userBest Practices
-
Fraud Prevention: Consider adding:
- Email verification before granting referral rewards
- IP/device tracking to prevent self-referrals
- Rate limiting on referral code generation
-
Credit Expiration: Implement expiration if needed:
- Add
expiresAtfield to credits - Run periodic cleanup jobs
- Add
-
Audit Trail: All credit changes are logged in
userCreditTransactionsfor compliance and debugging.
Onboarding System
ProductReady includes a user onboarding system to guide new users through key features. The onboarding state is stored in user_profiles.onboarding (JSONB).
Onboarding State Structure
interface OnboardingState {
/** Whether user dismissed referral prompt */
referral?: boolean;
gettingStarted: {
/** Whether user dismissed the Getting Started stepper */
dismissed: boolean;
steps: {
createPost: { done: boolean; doneAt: string | null };
managePosts: { done: boolean; doneAt: string | null };
shareReferral: { done: boolean; doneAt: string | null };
};
};
}Components
Getting Started Stepper
A dismissible progress card shown on the dashboard:
import { GettingStartedStepper } from "~/components/dashboard/getting-started-stepper";
<GettingStartedStepper />Features:
- 3-step progress tracker (Create Post → Manage Posts → Share Referral)
- Auto-completes steps based on user actions
- Can be manually dismissed by user
- Persists state to database
Referral Prompt
A sidebar card encouraging users to invite friends:
import { ReferralPrompt } from "~/components/dashboard/referral-prompt";
<ReferralPrompt onInviteClick={() => setShowReferral(true)} />Features:
- Only shows if user hasn't invited anyone (
onboarding.referral !== true) - Dismissible with persistence to database
- Auto-hides when user successfully invites someone
API Endpoints
Get Onboarding State
const state = await trpc.users.getOnboarding.query();
// Returns: { referral?: boolean, gettingStarted: {...} }Update Onboarding State
// Mark referral prompt as dismissed
await trpc.users.updateOnboarding.mutate({
onboarding: { referral: true },
});Getting Started Stepper API
// Get stepper state (onboarding router)
const state = await trpc.onboarding.getState.query();
// Complete a step
await trpc.onboarding.completeStep.mutate({ step: "createPost" });
// Dismiss the stepper
await trpc.onboarding.dismiss.mutate();Auto-Complete Hooks
Automatically mark steps as complete when user visits certain pages:
// In dashboard page
useOnboardingAutoComplete("createPost");
// In posts list page
useOnboardingAutoComplete("managePosts");
// In referral settings
useOnboardingStepComplete("shareReferral", () => {
// Triggered when user copies referral link
});Customization
To add new onboarding steps:
- Update
OnboardingStateinsrc/db/schema/user-profiles.ts - Add step definition to
STEPSarray ingetting-started-stepper.tsx - Add step key to
OnboardingStepSchemainsrc/server/routers/onboarding.ts - Create auto-complete hook in relevant pages
Important Fix
Referral Prompt Dismissal: When user clicks the close button on ReferralPrompt, the state must be persisted to database, not just local state:
// ✅ Correct: Persist to database
const handleDismiss = () => {
updateOnboardingMutation.mutate({
onboarding: { referral: true },
});
};
// ❌ Wrong: Only local state (bug fixed in 2025-12-18)
const handleDismiss = () => {
setDismissed(true); // Lost on page refresh!
};