Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/app/control/docs/money/referrals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,36 @@ Applies a referral code to the authenticated user.
- Invalid or expired codes should fail silently on the client to avoid UX disruption
- Referral earnings are calculated and paid out automatically by Echo

## Template Referrals

If you publish an Echo template on GitHub, you can automatically be registered as a referrer when someone scaffolds an app using your template via `echo-start`.

### How to Set Up

Add an `echo.json` file to the root of your template repository:

```json
{
"referralCode": "YOUR_REFERRAL_CODE"
}
```

When a user runs `npx echo-start -t https://github.com/you/your-template`, echo-start will detect your referral code and register you as the template referrer for their new app. You'll earn your referral percentage on all profits generated by that app.

### How It Works

1. Create a referral code for one of your apps (via the referral API or dashboard)
2. Add that code to `echo.json` in your template repository
3. When someone scaffolds from your template, echo-start sends the code to Echo
4. You're registered as the template referrer (first-come-first-served per app)
5. The `echo.json` file is removed from the scaffolded project automatically

### Important Notes

- Template referrals are separate from user-to-user referrals
- Only the first referral code registered per app is kept
- The referral code must be valid and not expired
Comment on lines +187 to +193

## Beta Status

This feature is in early beta and may not work as expected. Reach out in [Discord](https://discord.gg/merit) if you're interested in setting up referrals for your application.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "echo_apps" ADD COLUMN "templateReferrerCodeId" UUID;

-- AddForeignKey
ALTER TABLE "echo_apps" ADD CONSTRAINT "echo_apps_templateReferrerCodeId_fkey" FOREIGN KEY ("templateReferrerCodeId") REFERENCES "referral_codes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
5 changes: 4 additions & 1 deletion packages/app/control/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ model EchoApp {
currentReferralRewardId String? @db.Uuid // Reference to current active referral reward
currentReferralReward ReferralReward? @relation("CurrentReferralReward", fields: [currentReferralRewardId], references: [id])
ReferralRewards ReferralReward[] @relation("AppReferralRewards") // All referral rewards for this app
templateReferrerCodeId String? @db.Uuid // Referral code of the external template creator
templateReferrerCode ReferralCode? @relation("TemplateReferrer", fields: [templateReferrerCodeId], references: [id])
Comment on lines +109 to +110
appSessions AppSession[]
payouts Payout[]
OutboundEmailSent OutboundEmailSent[]
Expand Down Expand Up @@ -412,7 +414,8 @@ model ReferralCode {
createdAt DateTime @default(now()) @db.Timestamptz
updatedAt DateTime @updatedAt @db.Timestamptz

appMemberships AppMembership[]
appMemberships AppMembership[]
templateReferredApps EchoApp[] @relation("TemplateReferrer")

user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
Transaction Transaction[]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Template Referral API
*
* POST /api/v1/apps/template-referral - Register a template creator as the referrer for an app.
* Called by echo-start when scaffolding from an external template that includes a referral code.
* No auth required — the referral code itself authenticates the template creator.
*/
import { NextResponse } from 'next/server';
import { z } from 'zod';
import { createZodRoute } from '@/lib/api/create-route';
import { appIdSchema } from '@/services/db/apps/lib/schemas';
import { db } from '@/services/db/client';

const publicRoute = createZodRoute();

const templateReferralSchema = z.object({
echoAppId: appIdSchema,
referralCode: z.string().min(1),
});

export const POST = publicRoute
.body(templateReferralSchema)
.handler(async (_, context) => {
const { echoAppId, referralCode } = context.body;

const code = await db.referralCode.findUnique({
where: { code: referralCode },
});

if (!code || code.isArchived) {
return NextResponse.json(
{ success: false, message: 'Invalid referral code' },
{ status: 400 }
);
}

if (code.expiresAt < new Date()) {
return NextResponse.json(
{ success: false, message: 'Referral code has expired' },
{ status: 400 }
);
}

const app = await db.echoApp.findUnique({
where: { id: echoAppId },
});

if (!app) {
return NextResponse.json(
{ success: false, message: 'App not found' },
{ status: 404 }
);
}

// Only set template referrer if not already set (first-come-first-served)
if (app.templateReferrerCodeId) {
return NextResponse.json({
success: true,
message: 'Template referrer already registered',
});
}

await db.echoApp.update({
where: { id: echoAppId },
data: { templateReferrerCodeId: code.id },
});

return NextResponse.json({
success: true,
message: 'Template referrer registered successfully',
});
});
80 changes: 79 additions & 1 deletion packages/sdk/echo-start/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ import chalk from 'chalk';
import { spawn } from 'child_process';
import { Command } from 'commander';
import degit from 'degit';
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'fs';
import {
existsSync,
readdirSync,
readFileSync,
unlinkSync,
writeFileSync,
} from 'fs';
import path from 'path';

const program = new Command();
Expand Down Expand Up @@ -178,6 +184,55 @@ interface CreateAppOptions {
skipInstall?: boolean;
}

interface EchoTemplateConfig {
referralCode?: string;
}

const ECHO_CONTROL_URL = 'https://echo.merit.systems';

function readEchoTemplateConfig(projectPath: string): EchoTemplateConfig | null {
const configPath = path.join(projectPath, 'echo.json');
if (!existsSync(configPath)) return null;
try {
const parsed = JSON.parse(readFileSync(configPath, 'utf-8'));
if (
typeof parsed !== 'object' ||
parsed === null ||
Array.isArray(parsed)
) {
return null;
}
const code = parsed.referralCode;
if (code !== undefined && typeof code !== 'string') return null;
if (typeof code === 'string') {
return { referralCode: code.trim() };
}
return {};
} catch {
return null;
}
}

async function registerTemplateReferral(
appId: string,
referralCode: string
): Promise<boolean> {
try {
const response = await fetch(
`${ECHO_CONTROL_URL}/api/v1/apps/template-referral`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ echoAppId: appId, referralCode }),
}
Comment on lines +221 to +227
);
const data = (await response.json()) as { success?: boolean };
return data.success === true;
} catch {
return false;
}
}

function isExternalTemplate(template: string): boolean {
return (
template.startsWith('https://github.com/') ||
Expand Down Expand Up @@ -414,6 +469,29 @@ async function createApp(projectDir: string, options: CreateAppOptions) {
log.message(`Created .env.local with ${envVarName}`);
}

// Register template referral for external templates
if (isExternal) {
const echoConfig = readEchoTemplateConfig(absoluteProjectPath);
if (echoConfig?.referralCode) {
const registered = await registerTemplateReferral(
appId!,
echoConfig.referralCode
);
if (registered) {
log.message('Template creator registered as referrer');
}
}
// Always remove echo.json from the scaffolded project — it's only for referral tracking
try {
const echoConfigPath = path.join(absoluteProjectPath, 'echo.json');
if (existsSync(echoConfigPath)) {
unlinkSync(echoConfigPath);
}
} catch {
log.warning('Could not remove echo.json from scaffolded project');
}
}
Comment on lines +472 to +493

log.step('Project setup completed successfully');

// Auto-install dependencies unless skipped
Expand Down