vccs
Vue 3용 차트 컴포넌트 모음, Recharts에서 포팅
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Backend-first SaaS MVP that normalises activity from business tools into one organisation-scoped event stream.README 전문 보기 ↗Backend-first SaaS MVP that normalises activity from business tools into one organisation-scoped event stream.
GET/POST /api/events and POST /api/developer/seed-eventsStack: Next.js 16 App Router, React 19, TypeScript, Tailwind CSS 4, Supabase SSR/Auth/Postgres, Zod, Vitest.
Run npm install.
Copy values into an uncommitted .env.local:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-public-anon-key
NEXT_PUBLIC_SITE_URL=http://localhost:3000
GITHUB_TOKEN_ENCRYPTION_KEY=base64-encoded-32-byte-key
GOOGLE_CLIENT_ID=your-google-oauth-client-id
GOOGLE_CLIENT_SECRET=your-google-oauth-client-secret
GOOGLE_ANALYTICS_REDIRECT_URI=http://localhost:3000/auth/google-analytics/callback
Apply supabase/migrations/202607280001_mvp_support.sql using the Supabase SQL Editor, or link the Supabase CLI and run supabase db push.
In Supabase Auth, add http://localhost:3000/auth/callback to redirect URLs.
Also add http://localhost:3000/auth/github/callback for the GitHub connection flow.
Run npm run dev, then register. If email confirmation is enabled, confirm before signing in.
Generate the encryption key once and keep it only in server environment configuration:
openssl rand -base64 32
Changing this key makes existing GitHub credentials unreadable, so users must reconnect.
https://<project-ref>.supabase.co/auth/v1/callback (not the Metric Mage callback route).https://your-metric-mage-domain/auth/github/callback (and the localhost equivalent during development) to Supabase Authentication's redirect allow list.NEXT_PUBLIC_SITE_URL to the exact Metric Mage origin and set GITHUB_TOKEN_ENCRYPTION_KEY in the server environment.Metric Mage requests read:user repo so it can identify the account and read private repository activity and Actions runs. GitHub's classic repo scope is broad; a future GitHub App is recommended for finer-grained production permissions. Provider tokens are captured only by the server callback, verified with GET /user, encrypted using AES-256-GCM, and never returned to client code.
On Integrations, choose Connect GitHub, complete authorization, then choose Sync now. Sync imports up to 100 recent account activity records and up to 10 workflow runs for each of at most 10 repositories present in that activity. It translates pushes, opened/merged pull requests, opened issues, published releases, and completed workflows. GitHub's activity feed can lag, and only its recent window is available. Repeated syncs are safe because external identities are deterministic and protected by the existing unique index.
If Connect GitHub reports that the provider is disabled, query the Supabase project's Auth settings or reopen Authentication → Providers → GitHub and confirm the GitHub Enabled switch itself is on after saving the credentials. Saving a Client ID and secret does not necessarily enable the provider. In development, OAuth-start and callback failures include Supabase's safe error message and code; credentials and tokens are never logged.
Commands: npm run lint, npm run typecheck, npm test, npm run build.
createEvent normalises and validates input, verifies user membership and optional integration ownership, checks the (organisation, source, external ID) identity, and inserts through the user-scoped Supabase client. The partial unique index closes concurrent duplicate races. The UI never renders raw payloads. Metadata is displayed as escaped JSON.
Connectors implement IntegrationConnector; provider-specific translators implement EventTranslator. The GitHub connector is the first production implementation: it fetches GitHub records, translates supported records, and passes each normalised event to createEvent. The manual translator remains the minimal reference.
lib/integrations/registry.ts is the single catalogue for provider identity, presentation, capabilities, connector availability, OAuth metadata, and recommended schedule. The dashboard renders every card from this registry. GitHub is active; Google Analytics, Gmail, Google Calendar, Stripe, Shopify, Meta Ads, LinkedIn, Notion, and Slack are declared extension points; Manual routes to Developer Tools.
Every runtime connector implements connect, disconnect, refresh, sync, healthCheck, and translate. Connectors fetch and translate only—they never write business events. runIntegrationSync loads the correct connector, validates health, requests a batch of normalised events, passes those events through the universal event engine, counts imports/duplicates/unsupported records/errors, updates integration status, and writes a separate integration log.
To add a provider:
registry.ts.lib/integrations/<provider>.loader.ts.Scheduling is declarative only. Providers choose manual, hourly, daily, or webhook plus a human-readable recommendation. A future worker can enumerate connected integrations from the registry and invoke the same sync runner; no cron or queue is included yet.
Apply supabase/migrations/202607280002_integration_logs.sql before syncing. It adds the RLS-protected integration_logs table used for started/finished state, duration, received/imported/skipped/error counts, and rate-limit reporting.
GA4 uses a direct server-side Google authorization-code flow with state, PKCE, an exact redirect URI, and offline access. This was chosen instead of Supabase identity linking because Google Analytics needs a durable refresh token and Supabase does not manage provider-token refresh. Google client credentials, code exchange, provider tokens, discovery, and reporting never enter client-rendered data. Access and refresh tokens use the existing AES-256-GCM storage.
Create or select a project in Google Cloud Console.
Enable both Google Analytics Data API and Google Analytics Admin API.
Configure the OAuth consent screen. Choose Internal only for a Google Workspace organisation whose users should be the sole users; otherwise choose External.
If External remains in Testing, add every Google account that will connect as a test user. Refresh tokens for testing apps may have shorter lifetimes.
Add only these scopes: openid, email, and https://www.googleapis.com/auth/analytics.readonly.
Create an OAuth 2.0 Client ID with application type Web application.
Add http://localhost:3000 as an authorised JavaScript origin if Google requests one. Metric Mage itself performs OAuth on the server and does not depend on browser Google SDKs.
Add this exact authorised redirect URI:
http://localhost:3000/auth/google-analytics/callback
Put the client ID, client secret, and exact URI in .env.local using the variables above, then restart npm run dev.
For deployment, add the HTTPS production callback to the same Google OAuth client and set GOOGLE_ANALYTICS_REDIRECT_URI to that exact value. Common redirect_uri_mismatch causes are a missing port, HTTP versus HTTPS, a trailing slash, a different hostname, or changing the environment variable without restarting the server. Publish/verify the consent screen when leaving testing, following Google's current sensitive-scope verification requirements.
Connect Google Analytics from Integrations, approve only the read-only Analytics permission, and choose one discovered GA4 property. Account name, property name/ID, and property time zone are saved as non-secret integration settings. Change property does not delete historical events. Reauthorise replaces credentials; Disconnect attempts token revocation, clears encrypted credentials, and retains historical events.
Sync fetches aggregate-only GA4 reports for the latest seven complete property-local days and the preceding seven days. Metrics include active users, sessions, new/total users, engaged sessions, engagement rate, average session duration, event count, key events, page views, default channel group, source/medium, and landing page. No user-level or PII report is requested.
Generated event types:
analytics.traffic_increased / analytics.traffic_decreasedanalytics.conversions_increased / analytics.conversions_decreasedanalytics.engagement_rate_changedanalytics.organic_traffic_changedanalytics.paid_traffic_changedanalytics.referral_spikeanalytics.new_source_detectedanalytics.landing_page_surged / analytics.landing_page_declinedanalytics.tracking_inactiveThresholds live in lib/integrations/google-analytics/config.ts. Defaults require at least 100 baseline sessions, a 30-session absolute change and 20% relative change; conversion rules require at least five baseline key events and a difference of two; dimension and landing-page rules use their own conservative minimums. Tracking inactivity requires zero sessions after at least 100 baseline sessions. Metadata records inputs, periods, time zone, calculated changes, and thresholds. External IDs include property, rule, metric/dimension, and completed period, making repeat syncs duplicate-safe.
Known limitations: sync is manual, the reporting comparison is a conservative rolling 7-day model, only one property per organisation is active, report dimensions are bounded to 1,000 aggregate rows, and Google quota availability is external. Scheduling and webhooks remain framework-only.
RLS stays enabled. No service-role key, browser token input, dangerouslySetInnerHTML, or public ingestion endpoint exists. All organisation IDs originate from server-validated membership. GitHub tokens use authenticated server-side encryption and token columns are cleared on disconnect. The migration is additive and the original core tables/policies are assumed to exist remotely.
Live auth and RLS require configured credentials. Manually test isolation with two users: create separate organisations, add events/integrations in each, confirm neither appears in the other account, and directly query each table from both authenticated sessions. GitHub sync is manual and bounded; it does not use webhooks or background jobs. Organisation switching, invitations, other OAuth providers, queues, scheduled sync, billing, and AI are intentionally out of scope. Regenerate database types later with npx supabase gen types typescript --linked > types/database.ts.
Metric Mage now turns normalised events into deterministic, explainable insights. The engine is provider-independent: connectors still translate provider records into the universal events table, while isolated intelligence rules inspect those events and return insight candidates. The shared runner owns confidence clamping, fingerprints, duplicate-safe persistence, lifecycle updates, and recovery resolution.
Rules live under lib/intelligence/rules. Each rule declares a stable ID, priority, supported event providers, and a pure evaluate(events) function. Rules return a title, summary, severity, confidence (0–100), explanation, recommendation, evidence event IDs, and a deterministic fingerprint key. They never access Supabase or insert rows.
runIntelligence loads a rolling 30-day event window, evaluates the registry, and writes to insights. Repeated reasoning updates the matching (organisation_id, fingerprint) record instead of creating duplicates. Dismissed or resolved insight status is preserved on subsequent evaluation. An analytics recovery signal resolves active analytics-decline insights.
The initial rules cover deployments followed by traffic decline, failed workflows before traffic decline, traffic decline with stable conversions, repeated workflow failures, inactive analytics tracking, and analytics recovery. Thresholds are centralized in lib/intelligence/config.ts.
Intelligence runs after a successful integration sync and can also be started from Overview → Run intelligence. Overview, insight detail, and the combined Timeline query the universal insight model; there is no provider-specific dashboard.
GET /api/insights lists organisation-scoped insights (status, severity, and limit are optional).GET /api/insights/:id returns an insight with supporting events.POST /api/insights/:id/acknowledgePOST /api/insights/:id/dismissPOST /api/insights/:id/resolveAll endpoints require the current Supabase session and retain RLS organisation isolation.
Apply supabase/migrations/202607280003_insights.sql after the Phase 1–4 migrations. It creates the RLS-protected insights table, lifecycle and confidence constraints, evidence IDs, and a unique organisation/fingerprint constraint. Phase 5 adds no environment variables.
lib/intelligence/rules/<domain>.lib/intelligence/rules/index.ts.lib/intelligence/config.ts.One authenticated account can belong to multiple isolated organisations. The active workspace is resolved on the server from an HTTP-only ghost_active_organisation cookie, checked against active membership on every request, and backed by profiles.active_organisation_id so it survives login on another browser. If access disappears, Metric Mage safely selects the oldest remaining active membership.
The sidebar switcher submits a server action; it never treats client state as an authorization boundary. All dashboard, timeline, insight, integration, log, developer-tool and settings queries use the resulting organisation ID. OAuth callbacks also resolve the selected organisation, so credentials cannot land in a different workspace.
Permissions are defined centrally in lib/auth/permissions.ts.
Server actions and APIs validate permissions. Phase 6 RLS independently limits every business record by active organisation membership and restricts writes by role.
Owners and admins invite an email and select a non-owner role. Metric Mage stores only a SHA-256 token hash, expires invitations after seven days, and asks Supabase Auth to send a magic-link email whose safe callback returns to /invite/<token>. The invited user must authenticate as the invited email. Acceptance is an atomic security-definer function that checks email, expiry and duplicate membership, adds the membership, marks the invitation accepted and selects the new workspace.
Pending invitations can be resent (rotating the token and expiry) or cancelled. Duplicate pending invitations and existing members are rejected.
Apply supabase/migrations/202607280004_multi_organisation.sql. It adds organisation settings, the persisted profile preference, Manager role and member status, the invitation table/functions, teammate profile visibility, and canonical tenant RLS policies. No new environment variables are required. Supabase Auth email delivery and the existing NEXT_PUBLIC_SITE_URL must be configured.
Use getActiveOrganisation() in server pages/actions and organisationApiContext() in route handlers. Never accept an organisation ID from an untrusted body as the authority. Always scope provider credentials, events, logs and insights with ctx.organisation.id or ctx.organisationId, and check a named permission before mutations. Future billing, portals and agency reporting should attach to organisation_id; cross-organisation access must be an explicit separate capability.
Gmail is a real connector in the provider registry and shared sync runner. It uses Google OAuth 2.0 with PKCE, a ten-minute state cookie tied to the server-resolved active organisation, and only openid, email, and https://www.googleapis.com/auth/gmail.readonly. Google credentials are reused through GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET; GMAIL_REDIRECT_URI defaults to ${NEXT_PUBLIC_SITE_URL}/api/integrations/gmail/callback.
The first sync searches only the last seven days, excludes Spam and Trash, requests at most five pages and 250 messages, and fetches metadata format only. Later syncs use users.history.list and the stored history ID; an invalid history cursor falls back to the bounded recent query. Cursors update only after a successful connector run. Each mailbox is a separate integration record with independent encrypted access/refresh tokens, expiry, settings, health, cursor and logs.
Metric Mage stores subjects, safe address metadata, direction, labels, unread/starred flags, attachment filenames/counts and optionally a 200-character Gmail snippet. Snippets are off by default. It never downloads bodies, HTML, embedded images, attachments or remote links. Provider strings are control-character stripped, angle-bracket stripped and length bounded. Tokens, authorization codes and private snippets are never logged.
Events are gmail.message_received and gmail.message_sent. External IDs include integration ID, Gmail message ID and event type, so reruns and multiple mailboxes remain duplicate-safe. Spam/Trash and disabled directions are filtered. Gmail IMPORTANT remains metadata and is not intelligence.
http://localhost:3000/api/integrations/gmail/callback (or GMAIL_REDIRECT_URI) as an authorized redirect URI.Gmail will not work until those steps are complete. Safe defaults enable received, sent, unread and attachment metadata; snippets are disabled. The integration settings page supports each connected mailbox independently. Syncs are protected by a provider-agnostic ten-minute database lock that is released on completion/failure and expires after interrupted jobs.
Google Calendar is a read-only connector using the shared registry, encrypted credentials, organisation context, sync runner, logs and Phase 7 locks. OAuth uses PKCE/state cookies bound to the active organisation and the narrower calendar.events.readonly plus calendar.calendarlist.readonly scopes with openid email; no write scope is requested.
OAuth discovery stores up to 50 safe calendar descriptors. Primary and owned calendars default selected, except birthday/holiday calendars; shared/resource calendars default unselected. Each integration supports up to 20 selected calendars, five pages and 1,000 events per calendar. Full sync is bounded to 30 days past and 90 days ahead. Per-calendar sync tokens drive increments; HTTP 410 triggers a bounded full resync. Recurring instances use Google instance IDs, all-day events preserve exclusive date ends, and provider timezones remain metadata.
The connector emits google_calendar.event_created, google_calendar.event_updated, and google_calendar.event_cancelled. A bounded settings fingerprint map prevents unchanged events becoming updates; external IDs include integration, calendar, event, type and update fingerprint. Titles/locations/descriptions are sanitized and bounded. Descriptions are off by default; attendee lists, attachments, private properties and meeting access data are never stored.
Set GOOGLE_CALENDAR_REDIRECT_URI or use the documented localhost default. In Google Cloud enable Calendar API, add both read-only scopes, register http://localhost:3000/api/integrations/google-calendar/callback, retain Gmail/GA4 callbacks, configure test users, reauthorise, and obtain Workspace admin approval where required.
Stripe is a first-class connector in the provider registry. It connects existing Standard accounts with Stripe Connect OAuth, requests only scope=read_only, performs a bounded Events API import (30 days, 10 pages, 1,000 events), then reconciles the previous 72 hours. The verified Connect webhook feeds the same translator and universal event table; it stores only a minimal receipt, never a raw webhook.
Set STRIPE_PLATFORM_SECRET_KEY, STRIPE_CONNECT_CLIENT_ID, STRIPE_REDIRECT_URI, STRIPE_WEBHOOK_SECRET, and the server-only SUPABASE_SERVICE_ROLE_KEY. The pinned SDK is [email protected]; the explicit API version is 2026-03-25.dahlia. Production refuses a sk_test_ platform key.
In Stripe Dashboard, configure Connect for Standard accounts, add the exact development redirect http://localhost:3000/api/integrations/stripe/callback, and later add the production HTTPS equivalent. Add a Connect webhook endpoint at https://your-production-domain/api/webhooks/stripe, select “Events on connected accounts”, and subscribe only to the event names in lib/integrations/stripe/config.ts. Copy its distinct signing secret to the server environment. Start in test mode.
For local webhook testing:
stripe login
stripe listen --forward-connect-to localhost:3000/api/webhooks/stripe
Use the temporary whsec_... printed by that command only in local .env.local. The CLI’s fixtures do not reproduce every connected-account event.
PaymentIntent is the canonical successful/failed/cancelled payment lifecycle source. charge.succeeded and charge.failed are deliberately ignored. Checkout, invoice and subscription events remain lifecycle facts and do not add revenue. Monetary values remain integer minor units, include ISO currency, are grouped by currency by consumers, and are never converted. Customer IDs are one-way pseudonyms; emails, addresses, metadata, descriptions, payment methods, card/bank details, and raw Stripe objects are not persisted.
Apply supabase/migrations/202607280006_stripe_integration.sql before connecting Stripe. Owners/admins connect and disconnect; roles with integration.sync may reconcile. Disconnect clears local credentials while preserving historical events and receipts. Webhook processing is bounded and synchronous because Metric Mage does not yet have a durable job queue.
Search Console is a read-only connector in the shared provider registry. It reuses Google OAuth with PKCE, encrypted access/refresh tokens, organisation-bound callback cookies, provider-independent locks, the sync runner, logs and universal events. Required scope: openid email https://www.googleapis.com/auth/webmasters.readonly; no write scope is requested.
Enable Google Search Console API in the existing Google Cloud project, add the readonly scope to the OAuth consent screen, and register the exact redirect URI http://localhost:3000/api/integrations/google-search-console/callback. Set GOOGLE_SEARCH_CONSOLE_REDIRECT_URI server-side alongside the existing GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.
After connecting, /app/integrations/google-search-console/properties discovers Domain (sc-domain:) and URL-prefix properties, records their permission level, and lets an Owner/Admin select up to ten per Google account. Multiple Google accounts can create independent integrations. Managers can sync through the existing permission framework; Members/Viewers remain read-only.
Initial sync compares two bounded 45-day halves covering 90 days; later syncs compare complete seven-day periods after Search Console's three-day data delay. Limits are 10 selected properties, 250 final Search Analytics rows/property/period, 50 API requests, 10 URL inspections/property, 25 seconds and 500 translated events/property. Search data groups page, bounded query, country and device. Queries and URL paths are sanitised/length-limited; OAuth credentials are never logged.
Deterministic thresholds default to 25% and 10 previous clicks. Summary, click change, ranking change, new-query, sitemap and URL Inspection events use period/property/integration-bound external IDs. Dashboard cards show latest clicks, impressions, CTR and impression-weighted position. The supported Search Console APIs do not expose the Core Web Vitals report, so Metric Mage does not invent it. Index inspection is limited to top returned pages and can partially fail due to quota/permissions.
Troubleshooting: verify the API is enabled, the callback matches exactly, the account owns/has access to the property, and reconnect with prompt=consent if Google does not return a refresh token.
Shopify is a first-class connector using the versioned GraphQL Admin API 2026-07 (the REST Admin API is legacy). It requests only read_orders,read_products,read_inventory,read_discounts,read_locations. OAuth uses an organisation-bound, HttpOnly, one-use nonce, validates the exact *.myshopify.com host and verifies Shopify's callback HMAC before exchanging the code. Omitting access_mode=per-user requests an offline token; expiring offline access and refresh-token rotation are supported.
Configure a Shopify app in the Dev Dashboard with SHOPIFY_CLIENT_ID, SHOPIFY_CLIENT_SECRET, and exact allowed redirect http://localhost:3000/api/integrations/shopify/callback. Add the five read scopes. No write operation or write scope exists in Metric Mage.
The connector discovers store name, canonical myshopify domain, currency, IANA timezone and plan. Each store is a separate organisation-scoped integration. Initial order access is bounded to Shopify's standard 60-day order window (90 days requires separately approved read_all_orders, which Metric Mage deliberately does not request); reconciliation overlaps 72 hours. One GraphQL snapshot is capped at 100 orders/products/collections/discounts, 20 API requests, 25 seconds and 500 events. Existing locks, logs, encrypted credentials and duplicate protection are reused.
Events cover created/paid/cancelled/refunded/fulfilled orders, product state, zero/low inventory, collections and discounts. Dashboard/timeline consume normalised events without provider-specific storage. Customer references are salted hashes; only order count and country are retained. Metric Mage excludes names, emails, phones, addresses, notes, marketing preferences, payment/card data and raw Shopify payloads.
Troubleshooting: confirm the exact callback URL, read scopes, app installation on the development store, API version support, and store domain. Reinstall/reconnect after scope changes.
Provider ID meta_ads is a read-only connector built on the shared connector SDK. It uses a focused Zod-validated Graph API client, encrypted credentials, organisation-bound OAuth, generic locks/logs/health, isolated translation and universal event insertion. The default Graph/Marketing API is v25.0; override only with a validated META_GRAPH_API_VERSION.
http://localhost:3000/api/integrations/meta-ads/callback; production generally needs a public HTTPS equivalent.META_APP_ID, META_APP_SECRET, META_REDIRECT_URI and META_GRAPH_API_VERSION=v25.0.ads_read and business_management. The latter is used only for accessible business/ad-account discovery. No write permission or mutation exists.OAuth state is cryptographically random, HttpOnly, ten-minute, one-use, and bound to user, organisation, provider and return path. Callback exchanges short-lived access for a long-lived user token when supported, encrypts it, discovers identity/permissions/accounts and selects a sole eligible account only.
Initial sync retrieves 90 days. Normal sync reconciles seven days; settings retain revision fingerprints so unchanged rows create no event and changed attributed history creates a deterministic revision. A future scheduler can invoke the same runner for 28-day extended reconciliation. Daily reporting preserves account calendar date, timezone and currency. Spend/cost/value remain decimal strings plus six-place fixed micros; different currencies are never combined.
Hierarchy sync covers campaigns, ad sets and ads without audiences/targeting. Insights cover account and campaign daily reporting with explicit unified 7d_click and 1d_view windows. Meta-attributed purchases/leads use documented precedence (omni_purchase, pixel purchase, purchase) instead of summing aliases. They must not be described as Shopify orders, Stripe payments or proof of incremental impact.
Allowed breakdowns are publisher_platform, device_platform, and country, one at a time, though the default sync requests none. The client caps ten selected accounts, ten pages, 5,000 rows, 45 seconds and two retry slots. Errors distinguish authentication, permission, rate limit, invalid parameter and service failure. No leads, people, messages, comments, audiences, payment details, raw tokens or unfiltered Graph payloads are stored.
Disconnect uses the generic Owner/Admin action: credentials are erased and history is retained. Managers with integration.sync may manually sync; read-only roles may only view.
LinkedIn is implemented as a normal connector and feeds the universal event engine. It supports capability-aware LinkedIn Advertising and Company Page aggregate reporting, daily reconciliation, deterministic fingerprints, encrypted credentials, organisation permissions, sync locks, integration logs, and child-asset selection. It never posts, changes campaigns, retrieves member lists, or exposes an arbitrary API proxy.
Set LINKEDIN_CLIENT_ID, LINKEDIN_CLIENT_SECRET, and LINKEDIN_REDIRECT_URI (locally, exactly http://localhost:3000/api/integrations/linkedin/callback). LINKEDIN_API_VERSION defaults to 202607, the July 2026 stable Marketing API version selected from LinkedIn's official versioning documentation. Register the exact callback under the LinkedIn app Auth tab. Add the Sign In with LinkedIn using OpenID Connect product and apply separately for Advertising API and Community Management API access; creating an app alone does not grant Marketing API production access.
Metric Mage requests exactly openid profile r_ads r_ads_reporting r_organization_admin r_organization_social: OpenID identity, advertising structure, advertising reporting, administered-organisation/page analytics, and organisation content analytics respectively. The callback records what LinkedIn actually granted and probes Ads and Company Page capabilities independently. No write scope (rw_ads, w_organization_social, rw_organization_admin) is requested or used. Because LinkedIn consent is all-or-nothing for the requested scopes and scopes depend on approved products, the app may need the relevant LinkedIn product approvals before OAuth succeeds.
After connecting, choose eligible ad accounts and Company Pages under LinkedIn settings, then use the generic Sync now action. One eligible asset is selected automatically; multiple assets require explicit selection. Existing generic integrations.settings safely holds capability evidence, assets, selections, checkpoints, and fingerprints, so Phase 13 adds no database migration.
To add or update LinkedIn endpoints, keep them in the strict allowlist in lib/integrations/linkedin/client.ts, add Zod validation and bounded pagination, return only normalised records from the connector, and translate them in the isolated translator. Preserve aggregate-only privacy and currency separation.
Manual is a built-in, credential-free provider for structured first-party evidence that cannot be retrieved through an API. Enable it from Integrations, then create records or use the preview → mapping → validation → duplicate detection → import workflow at /app/integrations/manual. It feeds the same universal event stream as every connector; it is not a notes or spreadsheet system.
Supported record types are revenue, expense, lead, opportunity, customer, contract, quote, refund, stock adjustment, KPI snapshot, marketing spend, offline sale, phone enquiry, event attendance, donation and custom. Amount-bearing financial types require an amount and ISO three-letter currency. Records support status, category, tags, references, business-party/context fields and evidence attachment references.
CSV files must be UTF-8-compatible CSV, at most 2 MB and 5,000 rows. Quoted commas, escaped quotes, CRLF/LF and empty values are supported. Map at least title, recordType and occurredAt; unused columns are ignored. Valid rows import even when other rows fail. A validation report and import summary record successes, failures, duplicates and duration. Mappings are remembered by a deterministic header hash. Downloadable templates are provided for revenue, expenses, leads, marketing spend and KPI snapshots.
Duplicates use a SHA-256 fingerprint over type, occurred time, amount, currency, external reference and normalised title. Active-record uniqueness is also enforced in PostgreSQL, while every created, updated or archived revision generates a unique universal-event external ID. Archive is a soft operation and historical revisions remain readable.
Custom-field definitions are stored separately from typed values and support text, number, currency, date, checkbox and dropdown. Attachment rows store only private storage references, filename, approved MIME type and size; no file contents enter events. The current phase does not provide a storage uploader, OCR, formula engine, arbitrary spreadsheet editing or public attachment URLs.
Permissions: owner/admin can create, edit, import, archive and manage fields; manager can create, edit and import; viewer is read-only. The legacy member role is intentionally excluded from Manual writes. Every API derives the organisation from the authenticated active workspace and RLS repeats the tenant and role boundary.
Apply the migration before enabling Manual:
supabase db push
Or run supabase/migrations/202607290007_manual_data.sql through the Supabase SQL editor for the configured project.
Notion is a read-only connector that imports structured operational changes from explicitly shared data sources into universal events. It uses Notion OAuth, encrypted credentials, organisation-bound expiring state, generic integration logs/locks, child-resource selection and deterministic page snapshots. It never creates, updates or deletes Notion content and never fetches page bodies.
Configure a public Notion integration and set:
NOTION_CLIENT_ID=
NOTION_CLIENT_SECRET=
NOTION_REDIRECT_URI=http://localhost:3000/api/integrations/notion/callback
Register the redirect URI exactly in Notion. Give the integration Read content capability only. During authorization, choose the pages/databases to share. Notion exposes only content explicitly shared with the connection. Connect separately for each workspace; each authorization is stored as a distinct provider account where the workspace differs.
Metric Mage uses Notion-Version: 2026-03-11, the current official version at implementation time. Since modern Notion databases are containers, discovery and page queries use data sources. Select databases under /app/integrations/notion/settings; none are selected automatically.
The client supports title, rich text, status, select, multi-select, checkbox, date, people, number, URL, email, phone, relation, evaluated formula, created/edited time and created/edited by values. Unsupported properties are ignored safely. File contents and page blocks are never downloaded.
Initial sync imports pages edited within 90 days. Routine runs filter by the last completed edit checkpoint and compare deterministic snapshots. Only changed pages produce notion.page.*, notion.status.changed, notion.due_date.changed or notion.assignment.changed events. Existing events survive disconnect.
Owner/admin can connect, select data sources and disconnect; managers can sync/view; viewers are read-only. Existing integration and event RLS provides organisation isolation, so Phase 15 adds no migration.
Slack is a strictly read-only, polling connector for explicitly selected channels. OAuth v2 credentials are encrypted; workspace/channel/checkpoint/snapshot state uses generic organisation-scoped integration settings. Metric Mage does not request DMs, send messages, join channels, download files, fetch unfurls, score employees, infer sentiment or use Slack remote search.
Set SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET, and exact SLACK_REDIRECT_URI. SLACK_ENABLE_PRIVATE_CHANNELS=false is the safe default. Signing secret is reserved for a future signed Events API; Phase 16 intentionally uses polling and defines no event subscription.
Requested public bot scopes: channels:read, channels:history, users:read, reactions:read, team:read. Only when private access is explicitly enabled are groups:read and groups:history requested. No user scopes or write/DM/search scopes are requested.
Create a Slack app with this manifest:
display_information:
name: Metric Mage
description: Read-only Slack activity integration for Metric Mage
background_color: "#111111"
features:
bot_user:
display_name: Metric Mage
always_online: false
oauth_config:
redirect_urls:
- http://localhost:3000/api/integrations/slack/callback
scopes:
bot: [channels:read, channels:history, users:read, reactions:read, team:read]
settings:
org_deploy_enabled: false
socket_mode_enabled: false
token_rotation_enabled: true
Install the app, add it manually to each channel Metric Mage may read, reconnect to refresh discovery, then explicitly select channels in Metric Mage. Discovery is never treated as selection.
Initial sync defaults to 30 days (bounded to 90). Each invocation makes at most one 15-message history request and rotates fairly across selected channels. Cursors and timestamps persist. More work or a 429 response records a continuation at least 60 seconds later and returns a resumable partial result instead of holding a request open. Routine sync overlaps five minutes; deterministic snapshots suppress duplicates.
Thread parent metadata and reaction names/counts are stored. Full thread-body backfill is not enabled because Slack bot-token access varies and restrictive conversations.replies limits could starve recent messages. Files are counted but not downloaded; reacting-user lists, emails, phone numbers and full profiles are discarded.
No database migration is required. Existing integration/event/log RLS enforces organisation isolation.
Metric Mage can connect universal events across providers using reviewed, versioned rules. Correlations are evidence-backed associations, not causation claims. No LLM, embedding, fuzzy semantic match, or database-supplied executable rule is used.
The engine is split across lib/correlations: the registry discovers allowlisted rules, candidate selection bounds provider/event/time comparisons to 500 pairs per rule, scoring records every deterministic contribution, fingerprints suppress duplicates, the runner processes a maximum of 1,000 recent events by default, and the repository preserves evidence, revisions and invalidations. A failed rule is isolated from other rules.
Initial rules cover Shopify/Stripe payments and refunds, Meta Ads/Analytics, LinkedIn/Analytics, Search Console/organic Analytics, Gmail/Calendar, Gmail/Shopify, Notion/GitHub, Slack/Notion, Slack/GitHub, GitHub deployment/Analytics windows, and allowlisted manual transaction references. Temporal evidence contributes at most 20 points. Conflicting currencies or comparable unique identifiers reject a match. Public email domains do not establish a company identity.
Apply the additive schema:
npx supabase db push
Alternatively paste supabase/migrations/202607290008_correlations.sql into the Supabase SQL Editor. The migration creates rule settings, correlations, hashed evidence, immutable revisions, and run history with organisation indexes and RLS. It does not alter or delete existing events.
Owners and administrators can configure bounded thresholds and windows at /app/correlations/rules. Managers can run routine reconciliation. All organisation members with workspace access can inspect permitted evidence. A seven-day reconciliation is the default; the action bounds requests to 1–90 days and database reads to 5,000 events.
To add a rule, define a typed RuleSpec in lib/correlations/rules/index.ts, use explicit identifiers/URLs/amounts/attribution fields, assign a new immutable version, add false-positive tests, and keep prose in deterministic evidence templates. Database settings may only enable, disable, or parameterise that source-code rule.
Testing:
npm run lint
npm run typecheck
npm test
npm run build
Known limitations: processing is manually triggered until a background scheduler calls the same runner; historical backfills are bounded to 90 days per request; provider metadata quality limits which rules can match; temporal deployment/analytics edges are deliberately weak and hidden from the timeline by default.
/app/command-centre is the primary post-login operational dashboard. It composes bounded, organisation-scoped reads from universal events, correlations, integrations, integration logs, deterministic insights, correlation runs and memberships. It does not copy provider data into a parallel dashboard model and does not use AI or semantic search.
The command-centre modules in lib/command-centre provide:
Dashboard reads are bounded to 500 period events, 50 strong/moderate correlations, 30 alerts and small health/run summaries. Date ranges are Today, 7 days and 30 days. Search uses ordinary database ILIKE matching against imported organisation events. No remote provider search or semantic matching is performed.
Apply the additive saved-view migration:
npx supabase db push
The migration creates command_centre_views, enables RLS and allows all active organisation members to read the shared layout while only Owners and Administrators can create, change or delete it. No new environment variables are required.
Owners and Administrators can reorder, show, hide or collapse widgets, pin providers and choose the default range. Managers can run sync and reconciliation using existing permissions. Other workspace members receive read-only evidence views.
Alert rules are explicit: disconnected/expired integrations, failed syncs, tokens expiring within seven days, integrations without a successful sync for seven days, critical or named failure events, active warning/critical deterministic insights and failed correlation runs. Alert order is severity-based, not AI prioritisation.
The command centre never claims that a correlation is causation. Cross-platform activity shows only source/target edges already stored by Phase 17 and never inserts a missing intermediate step.
Metric Mage has a provider-neutral, organisation-scoped scheduler in lib/jobs. Connected providers register an integration.sync job from the provider registry; correlations, health, manual refresh and maintenance use the same execution path. The scheduler discovers due work while the executor owns distributed locking, timeout enforcement, deterministic retry, metrics and durable run logs. Connectors remain responsible for fetching and translation, and the existing sync runner remains responsible for event insertion and duplicate protection.
Apply supabase/migrations/202607290010_background_jobs.sql, set SUPABASE_SERVICE_ROLE_KEY and a long random BACKGROUND_JOB_SECRET, then invoke GET or POST /api/jobs/dispatch with Authorization: Bearer <secret>. Vercel Cron, Supabase Scheduled Functions or a future worker may call the same endpoint. Each invocation runs due jobs for up to four minutes (at most 100), four at a time; BACKGROUND_JOB_CONCURRENCY (1–8, default 4) adjusts parallelism. Invoke frequently (for example every five minutes); individual job schedules determine whether work is due.
Schedules support manual, immediate, recurring, cron, one_time and disabled. Recurring values use 5m, 1h, 1d or 1w syntax. Cron uses five fields and IANA timezone names. Retries default to three with deterministic exponential backoff; authentication, permission and configuration failures are never retried. Locks expire after timeout plus a recovery margin. Completed/skipped/cancelled run history is retained for 90 days by default. Owners/admins manage schedules, managers may run/cancel/retry, and viewers have read-only access at /app/jobs.
To add future work, register one stable organisation job key and a generic job type in lib/jobs/registry.ts, implement its bounded handler in lib/jobs/runner.ts, return record metrics, and add deterministic tests. Provider sync jobs require no custom scheduler code.
The Action Centre at /app/action-centre converts supported stored operational conditions into durable, organisation-scoped notifications. It uses a versioned rule registry in lib/notifications, stable SHA-256 fingerprints, structured evidence, append-only revisions, assignment history and explicit lifecycle transitions. No AI, embeddings, semantic ranking or provider calls are used.
Lifecycle states are open, acknowledged, snoozed, resolved and dismissed. Acknowledgement records awareness without claiming the condition is fixed. Critical escalation reopens a dismissed or snoozed item and clears its snooze. Identical evidence is idempotent. Automatic resolution occurs only when a successfully evaluated rule proves its condition no longer matches; the evidence and revisions remain. A condition that clears and later recurs creates a new lifecycle generation. A manually resolved item remains resolved while the exact condition continues; a dismissed continuing condition remains dismissed unless it becomes critical.
Built-in rules cover failed/retrying/stale background jobs, stored integration error states, explicit provider credential expiry, explicit rate limiting, high-score active deterministic correlations, rejected manual CSV rows, and missing fields already required by the organisation model. notification.generate runs every 15 minutes through the provider-neutral Phase 19 job runner with the same locks, retry and audit behavior.
Preferences follow: mandatory rule → user override → organisation default → system default. In-app notifications are implemented. Email and webhook preferences are stored for future delivery support. No outbound email or webhook delivery occurs in this phase.
Apply supabase/migrations/202607290011_notifications_action_centre.sql after the Phase 19 migration. Optional controls are NOTIFICATION_BATCH_SIZE (default 100, bounded 10–500), NOTIFICATION_CRITICAL_FAILURE_THRESHOLD (default 3), and NOTIFICATION_STALE_INTERVAL_MULTIPLIER (default 2). Existing server-role and background-job configuration remains required for scheduled evaluation.
Manual verification:
/app/action-centre./app/action-centre/preferences, /app/command-centre, /app/jobs, and /app/timeline.Metric Mage now includes versioned, provider-neutral operational workflows at /app/workflows, execution history at /app/workflow-runs, and a human approval inbox at /app/approvals. No AI, autonomous decisions, semantic matching or probabilistic branching is used.
Definitions are mutable only by creating a new immutable workflow_versions snapshot with immutable ordered steps. Every run pins that version and has a stable SHA-256 fingerprint derived from organisation, workflow, version, trigger, source and canonical payload. Replaying the same trigger returns the existing run.
Supported steps are task, approval, condition, delay, background job, notification, integration action, webhook, manual confirmation and complete. Conditions use explicit operators and fields. Retries use bounded exponential backoff. Runs pause durably for tasks, approvals and delays and resume through the Phase 19 dispatcher. Failure policies are fail, continue or pause.
Provider adapters expose only the action already supported consistently by connected Metric Mage providers: queueing their registered read-only sync job. No unsupported provider write operation is fabricated. Webhooks require a public HTTPS URL, have a 30-second maximum request timeout, and send only configured workflow data.
Built-in templates cover deployment failure review, integration reconnect, CSV import review, correlation review and background-job recovery. The builder supports drag ordering, JSON configuration validation, enable/disable/archive, duplication through templates, and immutable version history.
The background framework registers workflow.dispatch, workflow.timeout, approval.reminders and workflow.cleanup. Dispatch discovers bounded stored triggers for notification lifecycle events, correlation creation, failed jobs, integration state changes, CSV imports and hourly schedules. POST /api/workflows/trigger supports authenticated webhook triggers using the existing BACKGROUND_JOB_SECRET.
Apply supabase/migrations/202607290012_workflows.sql after Phase 20. No new environment variables are required.
Manual verification:
/app/workflows./app/workflow-runs/[id]./app/approvals/[id]./app/timeline./app/action-centre and workflow metrics appear in /app/command-centre./app/jobs.Metric Mage now includes organisation-scoped tasks, cases, checklists, dependencies,
assignments, watchers, comments/internal notes, evidence, immutable revisions,
templates, validated saved views, explicit SLA policies and idempotent recurrence.
Open /app/work for the unified inbox, or /app/tasks and /app/cases for focused
lists. Work management is deterministic and organisation-scoped. No AI is used to
create, prioritise or close work. SLA calculations use explicit stored policies.
Recurring task generation is idempotent.
Apply supabase/migrations/202607290013_tasks_cases_work_management.sql after the
Phase 21 workflow migration. The migration is additive and enables RLS on every
new table. The four WORK_* variables in .env.example are optional bounded
configuration and have safe defaults.
To add operational work from another subsystem, call the server-side work service with a stable source type, source ID, generation rule and template version. Automatic creation hashes those stable fields and returns the existing record on replay. Never create work directly from a browser database client.
Attachment metadata is supported, but binary upload/storage is deferred until a secure shared document abstraction is selected. Watchers provide in-app visibility; email and push delivery are not claimed. Scheduling uses the existing background job framework and may require an external scheduler on Vercel Hobby.