AI Marketing Platform - Technical Exploration
Deep-dive reference. For quick lookups (commands, env var list, DB table summary, publishing adapters), prefer
docs/QUICK_REFERENCE.md— it's shorter and updated more often. This doc goes one level deeper: full per-route breakdowns, the complete bot tool/cron inventory, and DB column detail. Last verified against the codebase 2026-09-13 (see #1251); if it drifts again, prefer the source (api/src/routes/,api/src/bot/,front-end/src/) over this file.
1. API Routes Structure (api/src/routes/)
The API registers 24 route plugins in api/src/index.ts, plus a handful of blog/* routes that run in a separate Fastify instance (buildBlogApp(), see api/src/blog/app.ts) for the multi-tenant hosted blog. The breakdown below covers the higher-traffic/content files in detail; the rest are summarized in the table at the end of this section.
posts.ts
Purpose: Post generation, regeneration, scheduling, publishing, and email templates.
Key Endpoints:
POST /generate_post— create posts for a campaign or customerPOST /bot/generate_post— worker-only bot-initiated generation (text/plain body)POST /post/init— create parent post record (returns post_id)POST /post/:id/generate— generate content for an existing postPOST /post/:id/platform/PUT /post/:id/platform— save platform-specific contentPOST /post/:platformPostId/generate-image— generate an image for one platform postPOST /regenerate_post_title,/edit_post— title regen and AI-powered content editingPOST /plan_posts,/plan_post_schedule— AI post planningPOST /publish_post— publish to a connected channelPOST /platform-post/:id/approve|reject|unapprove,/replayGET /post/:id— full post details with all platform contentGET /post/:id/platform/:platform/publish-previewPATCH /post/:id/schedule|unschedule,POST /post/:id/auto-schedule,/posts/schedule-bulk- Email template CRUD:
/template/preprocess|save-mappings|save-html|ai-edit|generate|generate-branded|ensure-default|apply-stock,GET /template/stock
Database Tables Used: customer_campaign, customer_posts, customer_platform_post, client_content_asset, integration_log, post_feedback, customer_integration, customer_email_template, customer_customer, email_lists, email_segments, customer_post_email_segments
clients.ts
Purpose: Client/brand CRUD, context, knowledge base, email marketing (contacts/lists/segments), analytics, onboarding. The largest route file — roughly 55 endpoints, grouped here by resource rather than listed exhaustively:
- Client data & context —
GET /clients,/clients/search,/client/:id/context|knowledge-base|content-memory|posts|posts/scheduled|posting-schedule|analytics|dashboard;PUT /client/:id/knowledge-base|posting-schedule/:platform;POST/PUT/PATCH /clientscreate/update,/clients/:id/channels,/client/:id/scrape-schedule - Onboarding —
POST /analyze-url,/onboarding/chat,/client/:id/first-post|from-conversation - Email marketing — contacts (
/client/:id/contacts,/contacts/import), lists/segments CRUD (/email-lists,/email-segments),POST /client/:id/email/send-test,/email-template/restore,PUT /client/:id/email-provider/* - Media & crawling —
POST /client/:id/media,/client/:id/crawl,/client/:id/content
Database Tables Used: messaging_users, client_knowledge_base, client_content_asset, post_analytics, channel_audience_snapshot, customer_platform_post, email_contacts, email_lists, email_list_members, email_segments, email_segment_members, email_send_log, email_tracking_events, customer_integration, customer_email_template, customer_customer, customer_site_page, post_generation_plan
telegram.ts
Purpose: Telegram bot pairing and notification preferences.
Key Endpoints:
POST /telegram/pair— pair Telegram chat to user account (DELETEto unpair)GET /telegram/status— current pairing statusPOST /telegram/pairing-token— generate a pairing tokenPATCH /telegram/preferences— update notification prefsPOST /client/:id/brand-voice— add brand voice rulePOST /post/approve— approve/reject/request-edits on a postGET /posts/pending-reminders— posts in review >24h (for cron)
Database Tables Used: messaging_users
integrations.ts
Purpose: OAuth flows and social/publishing credential management. PostPeer is the current unified social adapter — there is no "Outstand" integration in any route or in the publishing adapters (see docs/QUICK_REFERENCE.md's Publishing Adapters table for the current list). A couple of standalone scripts (api/scripts/outstand/get-post-details.ts, scripts/test-outstand.sh) still reference the old OUTSTAND_API_KEY/API, but nothing in the live route or adapter code calls them.
Key Endpoints:
POST /integrations/postpeer/auth-url,GET /client/:id/integrations/postpeer/accounts,PATCH .../accounts/:accountId,DELETE /client/:id/integrations/postpeerPOST /client/:id/analytics/sync-all,GET /client/:id/integrations/platformsPUT/DELETE /client/:id/integrations/mailchimp- Blogger:
GET /client/:id/integrations/blogger/auth-url(public),GET /integrations/blogger/callback(public) PUT/DELETE .../blogger|devto|shopify|webflow|ghost|wordpress;GET .../ghost|wordpress- Reddit:
GET .../reddit/auth-url,GET /integrations/reddit/callback(public),PUT/DELETE .../reddit - Google Search Console:
GET /integrations/google-search-console/auth|callback(public),POST .../refresh,GET .../keywords,DELETE ...
Database Tables Used: customer_integration, customer_posting_schedule, customer_customer, gsc_keywords_cache
tracking.ts
Purpose: Email open/click/unsubscribe tracking. All endpoints public (skipAuth), rate-limited.
Key Endpoints: GET /track/open, GET /track/click, GET /track/unsubscribe, GET /track/unsubscribe/info, POST /track/unsubscribe
webhooks.ts
Purpose: Inbound provider webhooks (public, signature-verified).
Key Endpoints: POST /webhooks/resend (email delivery/bounce/complaint/unsubscribe events → email_send_log), POST /webhooks/stripe (billing events)
admin.ts
Purpose: Admin-only management endpoints, gated by assertAdmin.
Key Endpoints: GET /admin/customers, /admin/users, /admin/users/:userId; POST /admin/customers/:id/trigger-briefing|generate-posts, /admin/impersonate; PUT /admin/users/:userId/subscription, /admin/posts/:id/exclude-from-count
Database Tables Used: customer_customer, customer_posts, messaging_users, user_subscription, user_reminder_log
content.ts
Purpose: Scraping, image search, and publishing helpers.
Key Endpoints: POST /scrape, /search_images, /publish, /publish/test-email, /generate_image
Other route files
| File | Purpose |
|---|---|
auth.ts | POST /auth/bot-token-exchange (Telegram-HMAC based, public) |
billing.ts | Stripe checkout/portal/cancel (/billing/checkout, /billing/portal, /billing/cancel) |
campaigns.ts | AI campaign proposals (/propose_campaign, /campaign_proposal) |
chat.ts | Web dashboard chat with the bot agent (POST /chat/send, GET /chat/commands) |
email-notifications.ts | GET /email/status, POST /email/enable |
forum.ts | Forum research (/client/:id/forum/searches, .../run, .../draft, .../suggest) |
health.ts | GET /health, GET /ready (both public) |
members.ts | Brand/company invites and membership management, incl. public GET /invites/:token |
platforms.ts | GET /platforms (static supported-platforms map, public) |
prompt-logs.ts | Admin LLM cost/log inspection |
public.ts | POST /public/register-interest (public, Turnstile-verified) |
referrals.ts | GET /referrals/me, POST /referrals/invite |
whatsapp.ts | WhatsApp webhook + pairing/status (public webhook endpoint) |
admin-cron-jobs.ts | GET /admin/cron-jobs, .../:jobName/runs — surfaces cron_job_run |
admin-signups.ts | Signup approval queue |
blog-admin.ts | Blog site/domain/post admin CRUD |
2. Main API Entry Point (api/src/index.ts)
Route registration (in order): postRoutes, campaignRoutes, clientRoutes, memberRoutes, contentRoutes, telegramRoutes, whatsappRoutes, emailNotificationRoutes, integrationRoutes, trackingRoutes, webhookRoutes, billingRoutes, healthRoutes, adminRoutes, promptLogsRoutes, adminCronJobsRoutes, adminSignupsRoutes, forumRoutes, authRoutes, platformRoutes, chatRoutes, referralRoutes, publicRoutes, blogAdminRoutes.
Auth hook (global preHandler, skips / and any route with config.skipAuth: true):
- Reads
Authorization: Bearer <token>orx-api-key. - Token matches
SUPABASE_SERVICE_ROLE_KEY→ treated as a worker; getscreateAdminSupabaseClient(). - Otherwise, tries local JWT verification (HS256 via
SUPABASE_JWT_SECRET, or ES256/RS256 via JWKS) to fast-path detectrole: service_role→ also gets the admin client. - Otherwise falls back to
supabase.auth.getUser(token)→ setsrequest.userandcreateUserSupabaseClient(token)(RLS-scoped), then runs theallowPendingUsersignup-approval gate.
CORS: origin is echoed back only when there's no Origin header (server/webhook calls), in non-production, when it matches FRONTEND_URL, or when it matches the *.ai-marketing-55e.pages.dev / *.ai-mee.uk pattern; otherwise 403. credentials: true; x-api-key is an allowed header.
Health checks: GET / (public, no auth logic at all — welcome message); GET /health and GET /ready in health.ts (both skipAuth).
3. Bot Module (api/src/bot/)
The Aimee bot runs in-process inside the API (BOT_ENABLED=true), not as a separate service. It is a single LangChain tool-calling agent — there is no multi-agent team, no MCP server, and no per-agent SKILL.md files or bot/data/config.json. Files: agent.ts, tools.ts, cron.ts, index.ts, memory.ts, logger.ts, connection-errors.ts, types.ts, channels/{telegram,whatsapp,email,telegram-markdown}.ts, prompts/{system,briefing,weekly,weekly-tips}.ts.
bot/index.ts
On boot, startBot() registers a ChannelAdapter per configured channel — Telegram (if TELEGRAM_BOT_TOKEN set), WhatsApp (if WHATSAPP_ACCESS_TOKEN + WHATSAPP_PHONE_NUMBER_ID set), Email (if RESEND_API_KEY set) — each wrapped with an outbound spam guard (withSendGuard), then starts them and wires handleIncomingMessage as the callback. stopBot() tears them down on SIGTERM/SIGINT. handleIncomingMessage resolves the paired Supabase user via messaging_users, prompts for pairing if unpaired, checks BOT_COMMANDS, else mints an RLS-scoped session and calls invokeAgent.
Bot commands:
/stop_reminders(alias "stop reminders") — disablesintegration_remindersin the customer's notification settings/add-note— appends a note to a client (customer_notes), auto-picking the client if the user only has one
Any other text is routed to the LangChain agent.
bot/agent.ts
Uses createToolCallingAgent + AgentExecutor from @langchain/classic/agents. Model comes from getModel('fast') (utils/llm-models.ts) — a ChatOpenAI client pointed at OpenRouter, defaulting to OPENROUTER_MODEL_FAST (google/gemini-3.5-flash-lite, env-overridable). Prompt is built from [system prompt, MessagesPlaceholder('chat_history'), human {input}, MessagesPlaceholder('agent_scratchpad')]; tools are bound via createTools(userId, contextClientId, db). The executor runs with maxIterations: 10 and surfaces tool failures back to the LLM as observations (buildToolRuntimeErrorObservation) instead of swallowing them. A BotDebugCallbackHandler logs every LLM/tool/chain event. One transient-LLM retry is allowed, but only before any tool has started.
bot/tools.ts — full tool list
search_customers, list_user_customers, fetch_client_context, update_company_info, create_notes, record_client_fact, list_campaigns, create_campaign, get_campaign_proposal, revise_campaign_proposal, edit_campaign_angle, add_campaign_angle, remove_campaign_angle, accept_campaign_proposal, reject_campaign_proposal, get_posting_schedule, set_posting_frequency, list_pending_posts, list_posts, list_scheduled_posts, get_post_status, create_post, save_platform_content, generate_content, approve_post, pending_reminders, schedule_post, schedule_multiple_posts, unschedule_post, update_post_status, get_post_link, client_dashboard, daily_briefing_summary, scrape_url, search_images, publish_content, add_brand_voice, get_site_links, search_docs — 39 tools total, each a DynamicStructuredTool wrapping a service-layer function directly (no MCP serialization). See api/src/bot/tools.ts for full input/output schemas.
bot/cron.ts — pg-boss scheduled jobs
Uses pg-boss, not node-cron. All schedules are in CRON_TIMEZONE (default UTC) and are upserted on boot; startup also runs a one-time teardown of an orphaned approval-reminders job from an earlier build.
| Job | Schedule | Purpose |
|---|---|---|
morning-briefing | Mon 8:30am | Weekly morning briefing to paired users |
weekly-summary | Fri 3:00pm | Weekly stats + AI narrative + tips |
website-rescrape | daily 3:00am | Re-crawl customer websites due for refresh |
email-segment-sync | daily 2:00am | Sync tag-based email behavioural segments |
social-analytics-sync | daily 4:00am | Sync social post analytics from PostPeer |
channel-audience-sync | daily 4:15am | Sync per-channel follower/subscriber counts |
schedule-replan | daily 5:00am | Rebalance provisional post slots |
knowledge-consolidation | daily 3:00am | Consolidate each client's knowledge base |
auto-generate-posts | daily 8:00am | Dispatch per-customer AI post generation |
plan-campaign-proposals | 1st & 15th, 7:00am | Fortnightly campaign proposal planning |
scheduled-publish | every 15 min | Publish approved posts whose slot is due |
integration-setup-reminders | daily 9:00am | Remind customers with incomplete integrations |
channel-health-alerts | daily 9:15am | Alert on expiring OAuth tokens / dark channels |
onboarding-reminders | daily 9:30am | Nudge users stalled at early onboarding |
trial-lifecycle | daily 10:00am | Trial-ending lifecycle nudges |
referral-nudge | Tue 11:00am | Weekly referral eligibility nudge |
prompt-log-cleanup | daily 1:00am | Delete expired prompt logs / log rows |
billing-reconcile | daily 6:00am | Safety net for dropped Stripe webhooks |
cron-health-watchdog | hourly | Alert on stale/silently-stopped jobs |
post-reconciliation | hourly :20 | Recover posts stranded in new after failed generation |
blog-domain-poll | every 5 min | Poll Cloudflare for custom-domain SSL status |
customer-post-generation is event-driven (fired once per active customer by auto-generate-posts), not separately scheduled.
bot/memory.ts
Conversation history lives in the chat_history Supabase table, keyed by (user_id, channel, external_id) — each user×channel pair is an independent thread. loadHistory fetches the most recent 20 messages (excluding one-way broadcasts like the morning briefing), truncates each to 4000 chars, and returns them as LangChain HumanMessage/AIMessage objects. saveHistory inserts one row per turn (non-fatal on failure, Sentry-captured).
Channel adapters
telegram.ts— Telegraf in long-polling mode. Listens for text messages,/start(pairing), and callback queries; routes each intohandleIncomingMessage.whatsapp.ts— a webhook adapter (no persistent connection); parses Meta's WhatsApp Cloud API payload and sends replies via the Graph API.email.ts— send-only via Resend; no inbound handling, used only for proactive notifications (briefings, summaries, nudges).
4. Database Schema (Key Tables)
| Table | Key Fields | Purpose |
|---|---|---|
messaging_users | user_id, channel, external_id, is_active, paired_at | Generic multi-channel pairing (Telegram, WhatsApp, etc.) — channel-generic from its original creation, not Telegram-specific |
chat_history | user_id, channel, external_id, role, content, metadata | Bot conversation turns |
customer_posts | customer_id, campaign_id, title, status, platforms, content_embedding | Parent "post idea" record driving per-platform generation |
customer_platform_post | customer_post_id, platform, content, review_status, scheduled_at, approved_* | Per-platform generated post instance with its full lifecycle |
client_brand_voice | customer_id, rule_type, rule_text, source | Brand voice rules applied during generation |
client_content_asset | customer_id, asset_type, url, content, metadata | Reference brand assets used as generation context |
post_analytics | platform_post_id, metric_type, metric_value | Engagement metrics per published post |
post_feedback | post_id, feedback_type, feedback_text, admin_* | Live — client/admin feedback feeding brand-voice learning |
customer_integration | customer_id, integration, access_token, metadata, needs_reconnect | OAuth/API connection state per customer per platform |
marketing_calendar | event_name, event_date, event_type, industries | Seasonal/industry calendar events |
integration_log | post_id, integration, result | Audit log of integration/publish attempts |
customer_customer | name, user_id, industries, company_id, brand_palette | Core brand/client record |
customer_campaign | customer_id, name, start_date/end_date, target_post_count, goal | Campaign grouping/goal container |
telegram_user_mapping and telegram_client_mapping did exist historically (_archive/20260309000000_phase2_tables.sql, _archive/20260322111410_remote_schema.sql) but were dropped in favor of messaging_users (telegram_user_mapping's drop migration is _archive/20260424000001_drop_telegram_user_mapping.sql) — messaging_users is the only one of the three that's live today.
5. Environment Configuration (api/src/config/env.ts)
NODE_ENV, PORT, HOST, LOG_LEVEL
# Supabase
SUPABASE_URL, SUPABASE_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_JWT_SECRET, DATABASE_URL
# LLM — OpenRouter primary, legacy fallbacks
LLM_PROVIDER (default ollama), OPENROUTER_LLM_ENABLED, OPENROUTER_API_KEY,
OPENROUTER_MODEL_REASONING, OPENROUTER_MODEL_CREATIVE, OPENROUTER_MODEL_FAST,
GOOGLE_API_KEY, OLLAMA_BASE_URL, OLLAMA_MODEL
# Image generation
IMAGE_GENERATION_ENABLED, IMAGE_GENERATION_MODEL, IMAGE_GENERATION_BUCKET,
SHARED_POST_IMAGES, IMAGE_TEXT_OVERLAY_ENABLED
# OAuth / third-party content
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, REDDIT_CLIENT_ID, REDDIT_CLIENT_SECRET,
SERPER_API_KEY, UNSPLASH_ACCESS_KEY
# Captcha
TURNSTILE_SECRET, HCAPTCHA_SECRET, HCAPTCHA_SITE_KEY
# Publishing
GHOST_API_URL, GHOST_ADMIN_API_KEY, POSTPEER_API_KEY
# Bot & channels
BOT_ENABLED, TELEGRAM_BOT_TOKEN, CRON_TIMEZONE,
WHATSAPP_ACCESS_TOKEN, WHATSAPP_PHONE_NUMBER_ID, WHATSAPP_PUBLIC_PHONE_NUMBER,
WHATSAPP_BUSINESS_ACCOUNT_ID, WHATSAPP_VERIFY_TOKEN, WHATSAPP_APP_SECRET
# Reconciliation sweep kill switches (default off — observe before arming)
RETIRE_STALE_POSTS, EXPIRE_UNREVIEWED_POSTS, EXPIRE_PARKED_POSTS,
EXPIRE_HELD_POSTS, OUTBOUND_SEND_GUARD_ENFORCE
# Email (Resend)
RESEND_API_KEY, RESEND_FROM_EMAIL, RESEND_FROM_NAME, RESEND_WEBHOOK_SECRET
# Billing (Stripe)
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
# Security / infra
INTEGRATION_ENCRYPTION_KEY, CHROMIUM_PATH, SENTRY_DSN,
LOG_PROMPTS, LOG_PROMPTS_DIR, LOG_PROMPT_METRICS, LLM_POST_COST_ALERT_USD
# URLs
API_BASE_URL, FRONTEND_URL, DOCS_URL
# Hosted blog
BLOG_PORT, BLOG_ROOT_DOMAIN, BLOG_CNAME_TARGET, PLATFORM_BLOG_HOST,
PLATFORM_BLOG_SITE_SLUG, INDEXNOW_ENABLED,
CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID, CLOUDFLARE_PURGE_TOKEN
Every var is read only via this module (env/dynamicEnv), never process.env directly elsewhere. See the inline comments in api/src/config/env.ts for rollout posture and gotchas on individual vars.
6. Front-end Architecture (front-end/)
Stack: Vue 3.5 (Composition API), Vite 6.2, Vue Router 4.6 (file-based via unplugin-vue-router), Pinia 3.0, TypeScript 5.8, Vitest, Playwright. Styling is Tailwind CSS v4 (@theme tokens in src/css/app.css) with shadcn-vue components (built on reka-ui) — there is no Bulma dependency anywhere in package.json. Path alias is /@src/ → src/. See docs/QUICK_REFERENCE.md for commands and the full directory-structure tree; this section covers current counts and inventories only.
Pages: 49 .vue files under src/pages/**. Layout wrappers exempt from being real routes: app.vue, auth.vue, chat.vue, [...all].vue. Groups: app/ (bulk of the app — admin/, client/[id]/ with 11 sub-pages, campaign/, campaigns/, posts/, plus top-level dashboard pages), auth/ (3), chat/ (1), invite/ (1), and a handful of root-level pages (ai-preview.vue, pending.vue, register-interest.vue, unsubscribe.vue, index.vue).
Pinia stores (src/stores/*.ts, 17): aimee-chat, blog, cadence, campaign-posts, campaigns, clients, email-contacts, email-templates, integrations, notes, notifications, outbox, plan, platforms, posts, seo, user-session.
Composables (src/composables/*.ts, 28): attribution, darkmode, dropdown, fetch, field-context, image-error, notyf, screen-size, supabase, theme-colors, useAnalyticsData, useAnalyticsVerdict, useBrandIntegrations, useBreadcrumbs, useClientIntegrations, useCrawlWatcher, useCrisp, useEmailListManagement, useEmailRequirements, useEmailTemplateEdit, useForumResearch, useGettingStarted, usePlatformAddRemove, usePlatformPostSnapshots, usePostRegeneration, useSocialConnect, user-token, vuero-context.
All of the above are auto-imported (unplugin-auto-import), along with Vue, VueUse, and Vue Router APIs — no explicit imports needed in .vue files.
Component convention: use shadcn-vue components (<Card>, <CardHeader>, <CardContent>, <CardFooter>, etc., auto-imported from src/components/ui/) — never a raw <div class="box"> or Bulma classes. See the root CLAUDE.md Styling section for the full convention.
Quick Reference
Bot architecture (single agent, no MCP):
Telegram/WhatsApp/Email
↓
handleIncomingMessage() — resolve messaging_users pairing
↓
LangChain tool-calling agent (bot/agent.ts)
↓ (binds tools from bot/tools.ts)
tool calls → service layer functions directly
↓
adapter.sendMessage() → save turn to chat_history
Cron: pg-boss, ~21 scheduled jobs plus 1 event-driven — see §3 above for the full table. Morning briefing is Monday 8:30am; weekly summary is Friday 3:00pm; there is no separate 10am "reminders" job (an old one by that name is torn down at boot).
Main database relations:
customer_customer(1) ← → (M)customer_postscustomer_posts(1) ← → (M)customer_platform_postcustomer_posts(1) ← → (M)integration_logcustomer_customer(1) ← → (M)client_brand_voicecustomer_customer(1) ← → (M)client_content_assetcustomer_customer(1) ← → (M)messaging_userscustomer_platform_post(1) ← → (M)post_analytics
Front-end: 49 pages, 17 Pinia stores, 28 composables. Tailwind CSS v4 + shadcn-vue (no Bulma). File-based routing via unplugin-vue-router.