Skip to main content

Sponsor Ads Schema Deep Dive

Overview

The sponsor ads module enables users to pay for promoted placement of items on the site. It implements a full lifecycle: submission, payment, admin review, active display, and expiration/cancellation. The system integrates with the payment provider infrastructure and supports both weekly and monthly sponsorship intervals.

Source file: template/lib/db/schema.ts


Table: sponsor_ads

Columns

ColumnDB NameTypeNullableDefaultConstraints
ididtextNocrypto.randomUUID()Primary Key
User/Submitter
userIduser_idtextNo-FK -> users.id (CASCADE)
Item
itemSlugitem_slugtextNo-Item being sponsored
Sponsorship Details
statusstatustext (enum)No'pending_payment'See status enum
intervalintervaltext (enum)No-weekly, monthly
Pricing
amountamountintegerNo-In dollars
currencycurrencytextNo'usd'ISO currency code
Payment
paymentProviderpayment_providertextNo-Provider name
subscriptionIdsubscription_idtextYes-External subscription ID
customerIdcustomer_idtextYes-External customer ID
Subscription Period
startDatestart_datetimestampYes--
endDateend_datetimestampYes--
Admin Review
reviewedByreviewed_bytextYes-FK -> users.id (SET NULL)
reviewedAtreviewed_attimestampYes--
rejectionReasonrejection_reasontextYes--
Cancellation
cancelledAtcancelled_attimestampYes--
cancelReasoncancel_reasontextYes--
Timestamps
createdAtcreated_attimestampNonow()-
updatedAtupdated_attimestampNonow()-

Foreign Keys

ColumnReferencesOn Delete
user_idusers.idCASCADE
reviewed_byusers.idSET NULL

Indexes

NameColumnsType
sponsor_ads_user_id_idxuserIdB-tree
sponsor_ads_item_slug_idxitemSlugB-tree
sponsor_ads_status_idxstatusB-tree
sponsor_ads_interval_idxintervalB-tree
sponsor_ads_provider_subscription_idx(paymentProvider, subscriptionId)Unique
sponsor_ads_start_date_idxstartDateB-tree
sponsor_ads_end_date_idxendDateB-tree
sponsor_ads_created_at_idxcreatedAtB-tree

Status Enum

export const SponsorAdStatus = {
PENDING_PAYMENT: 'pending_payment', // User submitted, waiting for payment
PENDING: 'pending', // User paid, waiting for admin review
REJECTED: 'rejected', // Admin rejected
ACTIVE: 'active', // Admin approved, displaying on site
EXPIRED: 'expired', // Subscription period ended
CANCELLED: 'cancelled' // Cancelled by user or admin
} as const;

export type SponsorAdStatusValues =
(typeof SponsorAdStatus)[keyof typeof SponsorAdStatus];

Interval Enum

export const SponsorAdInterval = {
WEEKLY: 'weekly',
MONTHLY: 'monthly'
} as const;

export type SponsorAdIntervalValues =
(typeof SponsorAdInterval)[keyof typeof SponsorAdInterval];

TypeScript Types

export type SponsorAd = typeof sponsorAds.$inferSelect;
export type NewSponsorAd = typeof sponsorAds.$inferInsert;

Status Workflow


Relations Diagram


Key Design Decisions

Two Foreign Keys to users

The table has two references to the users table:

  1. userId (CASCADE): The user who submitted and pays for the sponsorship. Deleting the user deletes all their sponsor ads.
  2. reviewedBy (SET NULL): The admin who approved/rejected. If the admin is deleted, the review record is preserved with a null reviewer.

External Payment Integration

The paymentProvider + subscriptionId combination has a unique index, mirroring the pattern in the subscriptions table. This allows looking up sponsor ads by their external subscription reference during webhook processing.

Amount in Dollars

Unlike the subscriptions table which stores amounts in cents, the sponsor_ads.amount column stores values in whole dollars. Be mindful of this difference when integrating with payment providers.


Query Examples

Create a sponsor ad request

import { db } from '@/lib/db/drizzle';
import { sponsorAds, SponsorAdStatus } from '@/lib/db/schema';

await db.insert(sponsorAds).values({
userId,
itemSlug: 'my-tool-slug',
status: SponsorAdStatus.PENDING_PAYMENT,
interval: 'monthly',
amount: 49,
currency: 'usd',
paymentProvider: 'stripe',
});

Confirm payment (update status)

await db
.update(sponsorAds)
.set({
status: SponsorAdStatus.PENDING,
subscriptionId: stripeSubscription.id,
customerId: stripeCustomer.id,
updatedAt: new Date(),
})
.where(eq(sponsorAds.id, sponsorAdId));

Admin approves a sponsor ad

await db
.update(sponsorAds)
.set({
status: SponsorAdStatus.ACTIVE,
reviewedBy: adminUserId,
reviewedAt: new Date(),
startDate: new Date(),
endDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
updatedAt: new Date(),
})
.where(eq(sponsorAds.id, sponsorAdId));

Admin rejects a sponsor ad

await db
.update(sponsorAds)
.set({
status: SponsorAdStatus.REJECTED,
reviewedBy: adminUserId,
reviewedAt: new Date(),
rejectionReason: 'Content does not meet advertising guidelines.',
updatedAt: new Date(),
})
.where(eq(sponsorAds.id, sponsorAdId));

Get active sponsor ads for display

import { and, eq, lte, gte } from 'drizzle-orm';

const now = new Date();
const activeAds = await db
.select()
.from(sponsorAds)
.where(
and(
eq(sponsorAds.status, SponsorAdStatus.ACTIVE),
lte(sponsorAds.startDate, now),
gte(sponsorAds.endDate, now)
)
);

Find sponsor ad by Stripe subscription ID

const ad = await db
.select()
.from(sponsorAds)
.where(
and(
eq(sponsorAds.paymentProvider, 'stripe'),
eq(sponsorAds.subscriptionId, stripeSubscriptionId)
)
)
.limit(1);

Get pending reviews for admin dashboard

const pendingReviews = await db
.select()
.from(sponsorAds)
.where(eq(sponsorAds.status, SponsorAdStatus.PENDING))
.orderBy(asc(sponsorAds.createdAt));

Expire sponsor ads past their end date

await db
.update(sponsorAds)
.set({
status: SponsorAdStatus.EXPIRED,
updatedAt: new Date(),
})
.where(
and(
eq(sponsorAds.status, SponsorAdStatus.ACTIVE),
lte(sponsorAds.endDate, new Date())
)
);

Design Notes

  • Item identification by slug. Like all item-referencing tables, itemSlug stores the item's content slug, not a database foreign key.
  • Unique provider+subscription index. Ensures no duplicate subscription records and enables fast webhook lookups.
  • Soft status transitions. Status changes are tracked via the status column itself. For a full audit trail, consider pairing with the activityLogs table.
  • Manual expiration. There is no automatic expiration mechanism at the database level. A cron job or webhook handler should periodically check for and expire past-due sponsor ads.