A media-side dashboard for publishers integrating the Ceed Ads SDK. Partners can self-serve onboarding, verify SDK integration, and monitor performance with 6 KPI metrics.
Live URL: https://ceed-publisher-console.vercel.app
For AI/Developer Context: See CONTEXT.md for system architecture, relationship with other Ceed repositories, and session history.
- Current Implementation Status
- Tech Stack
- Features
- Getting Started
- Project Structure
- Environment Variables
- Firestore Data Model
- API Endpoints
- Default Settings
- Known Limitations
| Feature | Status |
|---|---|
| Google Authentication | Implemented |
| Organization Management | Implemented |
| Team Management (invite, roles) | Implemented |
| Email Invitations (Firebase Trigger Email) | Implemented |
| App Creation & Management | Implemented |
| 6 KPI Analytics Dashboard | Implemented |
| Request/Event Logging | Implemented |
| Logs Explorer with Filtering | Implemented |
| CSV Export | Implemented |
| Integration Guide | Implemented |
| App Settings (cooldown, origins, languages, context mode) | Implemented |
| Light/Dark Mode | Implemented |
| Internationalization (i18n) | Implemented |
| User Settings (language, theme) | Implemented |
SDK Request API (/api/requests) |
Implemented |
SDK Event API (/api/events) |
Implemented |
- Framework: Next.js 14 (App Router)
- Language: TypeScript
- Styling: Tailwind CSS
- Authentication: Firebase Authentication (Google sign-in only)
- Database: Firestore
- State Management: TanStack Query (React Query)
- Validation: Zod
- Internationalization: next-intl
- Icons: Lucide React
- Date Handling: date-fns
- Deployment: Vercel
- Google sign-in only
- Server-side session cookies (5-day expiry)
- Protected routes via Next.js middleware
- Create organizations
- Switch between organizations (if user belongs to multiple)
- Organization-scoped data isolation
- Roles:
- Owner - Full access (manage org, apps, settings, members)
- Developer - Manage apps and settings, view analytics/logs
- Analyst - View analytics and logs only
- Invite members by email with automatic invitation emails
- Change member roles
- Remove members (with last-owner protection)
- Email Invitations:
- Uses Firebase Trigger Email Extension
- Invitation links with secure tokens (7-day expiration)
- Resend invitations for pending members
- Status badges (Pending/Active) on team page
- Create apps with name and platform selection (Web, iOS)
- View app list with status badges
- Configure app settings:
- Cooldown Seconds (0-3600, default: 30)
- Allowed Origins (CORS whitelist, default: empty = allow all)
- Supported Languages (eng, jpn)
- Context Logging Mode (none, truncated, hashed, full)
- Total Requests - All ad requests for the app
- Successful Requests - Requests that returned an ad
- Fill Rate - Successful requests / Total requests
- Total Impressions - Ad impression events
- Total Clicks - Ad click events
- Click-Through Rate (CTR) - Clicks / Impressions
Time Range Options: Today, 7 Days, 30 Days, 90 Days
- Requests Tab:
- Columns: Request ID, Status, Platform, Language, Response Time, Created At
- Filters: Status (All/Success/Error/No Fill), Platform (All/Web/iOS)
- Link to related events
- Events Tab:
- Columns: Event ID, Type, Request ID, Ad ID, Origin, Created At
- Filters: Event Type (All/Impression/Click)
- CSV Export for both tabs
- Displays App ID for SDK initialization
- Platform-specific setup instructions (Web/iOS)
- API endpoint documentation
- Light/Dark mode toggle
- Persisted via next-themes
- Multi-language UI support (English, Japanese)
- Language switching via user settings
- All UI text translatable via next-intl
- Locale files:
src/messages/en.json,src/messages/ja.json
- Default language preference (English/Japanese)
- Theme preference (Light/Dark/System)
- Persisted to localStorage
- TanStack Query (React Query) - All data fetching uses React Query with 30-minute staleTime for intelligent caching
- API Routes with Admin SDK - All Firestore access goes through authenticated API routes using Firebase Admin SDK
- No Redundant Fetches - Data persists across page navigation; only fetched once per session
- Smart Loading States - Only shows spinners on initial load; existing data remains visible during updates
- Cache Invalidation - After mutations (create/update/delete), cache is invalidated to fetch fresh data
- Node.js 18+
- Firebase project with:
- Authentication enabled (Google provider)
- Firestore database created
- Admin SDK credentials
- Trigger Email Extension installed and configured
# Clone the repository
git clone https://github.com/Ceed-dev/ceed-publisher-console.git
cd ceed-publisher-console
# Install dependencies
npm install
# Set up environment variables
cp .env.local.example .env.local
# Edit .env.local with your Firebase credentials
# Deploy Firestore indexes
firebase deploy --only firestore:indexes --project YOUR_PROJECT_ID
# Run development server
npm run dev- Create a Firebase project at https://console.firebase.google.com
- Enable Google Authentication provider
- Create a Firestore database
- Add your Vercel domain to authorized domains in Firebase Console
- Generate Admin SDK credentials (Project Settings > Service Accounts)
src/
├── app/ # Next.js App Router
│ ├── (auth)/login/ # Login page
│ ├── (dashboard)/ # Dashboard pages (with sidebar)
│ │ ├── apps/ # Apps list and management
│ │ │ ├── [appId]/ # App detail pages
│ │ │ │ ├── logs/ # Logs explorer
│ │ │ │ ├── settings/ # App settings
│ │ │ │ └── integration/ # Integration guide
│ │ │ └── new/ # Create app
│ │ ├── members/ # Team management
│ │ ├── settings/ # User settings
│ │ ├── organization/settings # Organization settings
│ │ └── organizations/new/ # Create organization
│ └── api/ # API routes
│ ├── auth/ # Session management
│ ├── dashboard/ # Dashboard APIs
│ ├── requests/ # SDK ad request endpoint
│ └── events/ # SDK event endpoint
├── components/ # React components
│ ├── ui/ # Base UI components
│ ├── layout/ # Sidebar, header, etc.
│ ├── analytics/ # KPI cards and grid
│ ├── logs/ # Log tables
│ └── apps/ # App-related components
├── lib/ # Utilities and helpers
│ ├── firebase/ # Firebase SDK initialization
│ ├── db/ # Firestore operations
│ ├── auth/ # Auth middleware
│ ├── validations/ # Zod schemas
│ └── utils/ # Helper functions
├── hooks/ # React hooks
├── contexts/ # React contexts
├── messages/ # i18n translation files (en.json, ja.json)
├── i18n.ts # i18n configuration
└── types/ # TypeScript types
# Firebase Client SDK
NEXT_PUBLIC_FIREBASE_API_KEY=
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=
NEXT_PUBLIC_FIREBASE_PROJECT_ID=
# Firebase Admin SDK
FIREBASE_ADMIN_PROJECT_ID=
FIREBASE_ADMIN_CLIENT_EMAIL=
FIREBASE_ADMIN_PRIVATE_KEY=
# Base URL for invitation links
NEXT_PUBLIC_BASE_URL=https://your-domain.com{
orgId: string;
name: string;
meta: {
createdAt: Timestamp;
updatedAt: Timestamp;
};
}{
memberId: string;
orgId: string;
userId: string;
email: string;
displayName?: string;
role: 'owner' | 'developer' | 'analyst';
status: 'pending' | 'active';
inviteToken?: string; // Secure token for invitation acceptance
inviteExpiresAt?: Timestamp; // 7-day expiration
invitedBy?: string; // userId of inviter
meta: {
createdAt: Timestamp;
updatedAt: Timestamp;
acceptedAt?: Timestamp; // When invitation was accepted
};
}{
to: string;
message: {
subject: string;
html: string;
text: string;
};
}{
appId: string;
orgId: string;
appName: string;
platforms: ('web' | 'ios')[];
status: 'active' | 'suspended';
settings: {
cooldownSeconds: number; // default: 30
allowedOrigins: string[]; // default: []
supportedLanguages: ('eng' | 'jpn')[]; // default: ['eng']
contextLoggingMode: 'none' | 'truncated' | 'hashed' | 'full'; // default: 'none'
};
meta: {
createdAt: Timestamp;
updatedAt: Timestamp;
};
}{
requestId: string;
appId: string;
status: 'success' | 'error' | 'no_fill';
platform: 'web' | 'ios';
language: 'eng' | 'jpn';
userAgent?: string;
origin?: string;
contextText?: string;
contextTextHash?: string;
contextTextMode?: 'truncated' | 'hashed' | 'full';
errorCode?: string;
errorMessage?: string;
responseTimeMs?: number;
meta: {
createdAt: Timestamp;
};
}{
eventId: string;
appId: string;
requestId: string;
eventType: 'impression' | 'click';
adId?: string;
origin?: string;
userAgent?: string;
meta: {
createdAt: Timestamp;
};
}{
auditId: string;
orgId: string;
appId?: string;
userId: string;
userEmail: string;
action: string;
changes: Record<string, unknown>;
meta: {
createdAt: Timestamp;
};
}| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/session |
Create session from Firebase ID token |
| POST | /api/auth/logout |
Clear session cookie |
| GET/POST | /api/dashboard/organizations |
List/Create organizations |
| GET/PATCH | /api/dashboard/organizations/[orgId] |
Get/Update organization |
| GET/POST | /api/dashboard/apps |
List/Create apps |
| GET/PATCH | /api/dashboard/apps/[appId] |
Get/Update app |
| GET | /api/dashboard/apps/[appId]/analytics |
Get analytics metrics |
| GET | /api/dashboard/apps/[appId]/logs/requests |
List request logs |
| GET | /api/dashboard/apps/[appId]/logs/events |
List event logs |
| GET/POST | /api/dashboard/members |
List/Invite members |
| PATCH/DELETE | /api/dashboard/members/[memberId] |
Update/Remove member |
| POST | /api/dashboard/members/[memberId]/resend |
Resend invitation email |
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/invites/[token] |
Validate invitation token |
| POST | /api/invites/[token]/accept |
Accept invitation (requires auth) |
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/requests |
Ad request endpoint |
| POST | /api/events |
Event tracking (impression/click) |
// POST /api/requests
{
appId: string;
platform: 'web' | 'ios';
language: 'eng' | 'jpn';
contextText?: string;
}
// Response
{
requestId: string;
ad: {
adId: string;
type: string;
content: {
title: string;
description: string;
imageUrl: string;
clickUrl: string;
};
};
}// POST /api/events
{
appId: string;
requestId: string;
eventType: 'impression' | 'click';
}
// Response
{
eventId: string;
}When a new app is created, the following defaults are applied:
| Setting | Default Value |
|---|---|
cooldownSeconds |
30 |
allowedOrigins |
[] (allows all origins) |
supportedLanguages |
['eng'] |
contextLoggingMode |
'none' |
- Custom date range picker - Only preset time ranges available
- Cooldown enforcement -
cooldownSecondssetting is stored but not enforced in/api/requests - Event validation - Events are accepted without validating requestId/adId match
- Additional log filters - conversationId, messageId, sdkVersion filters not implemented
- Rate limiting - No rate limiting on SDK endpoints
- Audit log viewer - Audit logs are created but not viewable in UI
| Spec | Current Implementation |
|---|---|
| Request statuses: success, no_ad, error | success, error, no_fill |
| Default cooldownSeconds: 60 | 30 |
| Default supportedLanguages: ['eng', 'jpn'] | ['eng'] |
| Default contextLoggingMode: 'truncated' | 'none' |
| Default allowedOrigins: ['*'] | [] (same effect) |
| Members page: Owner only | All roles can view |
# Run development server
npm run dev
# Build for production
npm run build
# Run linter
npm run lint
# Deploy Firestore indexes
firebase deploy --only firestore:indexes --project YOUR_PROJECT_IDPrivate - Ceed Dev