Skip to content
Draft
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
4 changes: 2 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ Domain language for how a fresh copy of vibestack becomes someone's product —
- **Auth** (`packages/auth`) — sign-in/up, sessions, OAuth, magic links, passkeys, 2FA, password reset, email verification. Owns the `user`, `session`, `account`, `verification`, `passkey`, `two_factor` tables.
- **Org** (`packages/auth/plugin organization`) — orgs, members, invitations. Owns the `organization`, `member`, `invitation` tables.
- **Billing** (`packages/billing` + `packages/db/schema/billing.ts`) — Polar customer + subscription mirror. Listens for Polar webhooks.
- **Audit** (`packages/db/schema/audit.ts`) — append-only log. Written from server-side action handlers; never directly by clients.
- **Audit** (`packages/db/schema/audit.ts`) — append-only log. Written from server-side action handlers via the `recordAuditLog()` seam (`packages/db/src/audit.ts`); never directly by clients.
- **Identity surface** (apps) — `web`, `marketing`, `admin` share auth via Better Auth cookies on the same root domain.

## Invariants

- A `Member` always belongs to exactly one `Organization` and exactly one `User`.
- A `Session` is invalidated on user ban, password change, or explicit revoke.
- `AuditLog` rows are immutable once written. No `UPDATE`/`DELETE` allowed.
- `AuditLog` rows are immutable once written. No `UPDATE`/`DELETE` allowed. All writes go through `recordAuditLog()` (`@vibestack/db`) — the only sanctioned write path; never `db.insert(auditLog)` directly outside that seam (the seed script excepted).
- All emails are sent via `@vibestack/email` (Resend). Never call the email provider directly from app code.
- All R2 access goes through `@vibestack/storage`. Never instantiate an S3 client elsewhere.
- All cross-app API calls go through tRPC procedures in `packages/api`. No direct DB queries from `apps/marketing`.
Expand Down
22 changes: 8 additions & 14 deletions apps/admin/src/app/[locale]/feature-flags/_actions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"use server";

import "server-only";
import { randomUUID } from "node:crypto";
import { db } from "@vibestack/db";
import { auditLog } from "@vibestack/db/schema/audit";
import { recordAuditLog } from "@vibestack/db";
import { toggleFlag } from "@vibestack/feature-flags/admin";
import { revalidatePath } from "next/cache";

Expand All @@ -21,17 +19,13 @@ export async function toggleFlagAction(input: {
if (!result.ok) {
return { ok: false, error: result.error };
}
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: session.user.id,
action: "feature_flag.toggled",
targetType: "feature_flag",
targetId: String(input.id),
metadata: { key: input.key, active: input.active },
})
.onConflictDoNothing();
await recordAuditLog({
action: "feature_flag.toggled",
actorUserId: session.user.id,
targetType: "feature_flag",
targetId: String(input.id),
metadata: { key: input.key, active: input.active },
});
revalidatePath("/[locale]/feature-flags", "page");
return { ok: true };
}
22 changes: 8 additions & 14 deletions apps/admin/src/app/[locale]/users/_actions.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"use server";

import "server-only";
import { randomUUID } from "node:crypto";
import { auth } from "@vibestack/auth";
import { db } from "@vibestack/db";
import { auditLog } from "@vibestack/db/schema/audit";
import { recordAuditLog } from "@vibestack/db";
import { revalidatePath } from "next/cache";
import { headers } from "next/headers";

Expand All @@ -18,17 +16,13 @@ async function audit(
targetUserId: string,
metadata: Record<string, unknown> = {},
): Promise<void> {
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: actorId,
action,
targetType: "user",
targetId: targetUserId,
metadata,
})
.onConflictDoNothing();
await recordAuditLog({
action,
actorUserId: actorId,
targetType: "user",
targetId: targetUserId,
metadata,
});
}

function callerHeaders() {
Expand Down
22 changes: 8 additions & 14 deletions apps/web/src/app/api/webhooks/polar/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
// dashboard delivers events via Svix; we re-implement Svix's signature
// algorithm inline so we don't pull a heavy dep.

import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
import { db } from "@vibestack/db";
import { auditLog } from "@vibestack/db/schema/audit";
import { createHmac, timingSafeEqual } from "node:crypto";
import { db, recordAuditLog } from "@vibestack/db";
import { polarCustomer, subscription } from "@vibestack/db/schema/billing";
import { env } from "@vibestack/env/server";
import { eq } from "drizzle-orm";
Expand Down Expand Up @@ -144,17 +143,12 @@ async function upsertSubscription(payload: PolarSubscription): Promise<void> {
}

async function audit(action: string, targetId: string, metadata: unknown) {
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: null,
action,
targetType: "polar",
targetId,
metadata: metadata as Record<string, unknown>,
})
.onConflictDoNothing();
await recordAuditLog({
action,
targetType: "polar",
targetId,
metadata: metadata as Record<string, unknown>,
});
}

export async function POST(request: Request) {
Expand Down
39 changes: 17 additions & 22 deletions apps/web/src/app/api/webhooks/resend/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
import { db } from "@vibestack/db";
import { auditLog } from "@vibestack/db/schema/audit";
import { createHmac, timingSafeEqual } from "node:crypto";
import { db, recordAuditLog } from "@vibestack/db";
import { user as userTable } from "@vibestack/db/schema/auth";
import { env } from "@vibestack/env/server";
import { eq } from "drizzle-orm";
Expand Down Expand Up @@ -148,25 +147,21 @@ export async function POST(request: Request) {
const recipient = recipientOf(payload);
const actorUserId = await actorIdFor(recipient);

await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId,
action: actionFor(payload.type),
targetType: "email",
targetId: payload.data?.email_id ?? null,
metadata: {
to: recipient,
from: payload.data?.from,
subject: payload.data?.subject,
bounce: payload.data?.bounce,
complaint: payload.data?.complaint,
svixId: id,
createdAt: payload.created_at,
},
})
.onConflictDoNothing();
await recordAuditLog({
action: actionFor(payload.type),
actorUserId,
targetType: "email",
targetId: payload.data?.email_id ?? undefined,
metadata: {
to: recipient,
from: payload.data?.from,
subject: payload.data?.subject,
bounce: payload.data?.bounce,
complaint: payload.data?.complaint,
svixId: id,
createdAt: payload.created_at,
},
});

return NextResponse.json({ ok: true });
}
21 changes: 8 additions & 13 deletions apps/web/src/app/onboarding/_actions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"use server";

import "server-only";
import { randomUUID } from "node:crypto";
import { auth } from "@vibestack/auth";
import { db } from "@vibestack/db";
import { db, recordAuditLog } from "@vibestack/db";
import { auditLog } from "@vibestack/db/schema/audit";
import { and, eq } from "drizzle-orm";
import { headers } from "next/headers";
Expand Down Expand Up @@ -38,17 +37,13 @@ export async function completeOnboardingAction(input: {
if (already.length > 0) {
return { ok: true };
}
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: user.id,
action: ONBOARDING_DONE_ACTION,
targetType: "user",
targetId: user.id,
metadata: { skipped: input.skipped ?? false },
})
.onConflictDoNothing();
await recordAuditLog({
action: ONBOARDING_DONE_ACTION,
actorUserId: user.id,
targetType: "user",
targetId: user.id,
metadata: { skipped: input.skipped ?? false },
});
return { ok: true };
} catch (err) {
return {
Expand Down
72 changes: 27 additions & 45 deletions packages/api/src/affiliate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@

import "server-only";
import { randomBytes, randomUUID } from "node:crypto";
import { db } from "@vibestack/db";
import { db, recordAuditLog } from "@vibestack/db";
import {
affiliate,
affiliateClick,
affiliatePayout,
affiliateSignup,
} from "@vibestack/db/schema/affiliate";
import { auditLog } from "@vibestack/db/schema/audit";
import { env } from "@vibestack/env/server";
import { and, count, desc, eq, inArray } from "drizzle-orm";

Expand Down Expand Up @@ -40,17 +39,13 @@ export async function enroll(opts: { userId: string }): Promise<{
code,
commissionRate: env.AFFILIATE_DEFAULT_RATE,
});
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: opts.userId,
action: "affiliate.enrolled",
targetType: "affiliate",
targetId: id,
metadata: { code },
})
.onConflictDoNothing();
await recordAuditLog({
action: "affiliate.enrolled",
actorUserId: opts.userId,
targetType: "affiliate",
targetId: id,
metadata: { code },
});
return { id, code };
}

Expand Down Expand Up @@ -116,16 +111,12 @@ export async function attributeSignup(opts: {
referredUserId: opts.referredUserId,
})
.onConflictDoNothing();
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: opts.referredUserId,
action: "affiliate.signup",
targetType: "affiliate",
targetId: aff.id,
})
.onConflictDoNothing();
await recordAuditLog({
action: "affiliate.signup",
actorUserId: opts.referredUserId,
targetType: "affiliate",
targetId: aff.id,
});
}

export async function stats(affiliateId: string): Promise<{
Expand Down Expand Up @@ -186,17 +177,12 @@ export async function requestPayout(opts: {
affiliateId: opts.affiliateId,
amountCents: opts.amountCents,
});
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: null,
action: "affiliate.payout.requested",
targetType: "affiliate_payout",
targetId: id,
metadata: { amountCents: opts.amountCents },
})
.onConflictDoNothing();
await recordAuditLog({
action: "affiliate.payout.requested",
targetType: "affiliate_payout",
targetId: id,
metadata: { amountCents: opts.amountCents },
});
return { id };
}

Expand Down Expand Up @@ -227,17 +213,13 @@ export async function decidePayout(opts: {
decidedAt: new Date(),
})
.where(eq(affiliatePayout.id, opts.id));
await db
.insert(auditLog)
.values({
id: randomUUID(),
actorUserId: opts.actorId ?? null,
action: `affiliate.payout.${opts.status}`,
targetType: "affiliate_payout",
targetId: opts.id,
metadata: { polarPayoutId: opts.polarPayoutId },
})
.onConflictDoNothing();
await recordAuditLog({
action: `affiliate.payout.${opts.status}`,
actorUserId: opts.actorId ?? null,
targetType: "affiliate_payout",
targetId: opts.id,
metadata: { polarPayoutId: opts.polarPayoutId },
});
}

export async function topReferrers(limit = 100) {
Expand Down
Loading
Loading