# MakinForU Panel – Full documentation for LLMs MakinForU Panel is an open-source web and product analytics platform β€” a privacy-first alternative to Mixpanel and Google Analytics, with optional self-hosting. This file contains the full text of all documentation pages. Each section is separated by --- and includes a canonical URL. --- ## What is MakinForU? URL: https://panel.makinforu.com/docs ✨ Key Features [#-key-features] * **πŸ” Advanced Analytics**: [Funnels](/features/funnels), cohorts, user profiles, and session history * **🎬 Session Replay**: [Record and replay user sessions](/features/session-replay) with privacy controls built in * **πŸ“Š Real-time Dashboards**: Live data updates and interactive charts * **🎯 A/B Testing**: Built-in variant testing with detailed breakdowns * **πŸ”” Smart Notifications**: Event and funnel-based alerts * **🌍 Privacy-First**: Cookieless tracking and GDPR compliance * **πŸš€ Developer-Friendly**: Comprehensive SDKs and API access * **πŸ“¦ Self-Hosted**: Full control over your data and infrastructure * **πŸ’Έ Transparent Pricing**: No hidden costs * **πŸ› οΈ Custom Dashboards**: Flexible chart creation and [data visualization](/features/data-visualization) * **πŸ“± Multi-Platform**: Web, mobile (iOS/Android), and server-side tracking * **πŸ€– MCP Server**: [Ask your AI about your users](/features/mcp) β€” connect Claude, Cursor, or any MCP client * **πŸ’° Revenue Tracking**: [Monitor purchases and subscriptions](/features/revenue-tracking) alongside product events * **πŸ”Œ Integrations**: [Google Search Console](/features/integrations), and more πŸ“Š Analytics Platform Comparison [#-analytics-platform-comparison] | Feature | MakinForU | Mixpanel | GA4 | Plausible | | ----------------------------------------- | --------- | --------- | ----- | --------- | | βœ… Open-source | βœ… | ❌ | ❌ | βœ… | | 🧩 Self-hosting supported | βœ… | ❌ | ❌ | βœ… | | πŸ”’ Cookieless by default | βœ… | ❌ | ❌ | βœ… | | πŸ” Real-time dashboards | βœ… | βœ… | ❌ | βœ… | | πŸ” Funnels & cohort analysis | βœ… | βœ… | βœ…\* | βœ…\*\*\* | | πŸ‘€ User profiles & session history | βœ… | βœ… | ❌ | ❌ | | 🎬 Session replay | βœ… | βœ…\*\*\*\* | ❌ | ❌ | | πŸ“ˆ Custom dashboards & charts | βœ… | βœ… | βœ… | ❌ | | πŸ’¬ Event & funnel notifications | βœ… | βœ… | ❌ | ❌ | | 🌍 GDPR-compliant tracking | βœ… | βœ… | ❌\*\* | βœ… | | πŸ“¦ SDKs (Web, Swift, Kotlin, ReactNative) | βœ… | βœ… | βœ… | ❌ | | πŸ’Έ Transparent pricing | βœ… | ❌ | βœ…\* | βœ… | | πŸš€ Built for developers | βœ… | βœ… | ❌ | βœ… | | πŸ”§ A/B testing & variant breakdowns | βœ… | βœ… | ❌ | ❌ | βœ…\* GA4 has a free tier but often requires BigQuery (paid) for raw data access. ❌\*\* GA4 has faced GDPR bans in several EU countries due to data transfers to US-based servers. βœ…\*\*\* Plausible has simple goals βœ…\*\*\*\* Mixpanel session replay is limited to 5k sessions/month on free and 20k on paid. MakinForU has no limit. πŸš€ Quick Start [#-quick-start] Before you can start tracking your events you'll need to create an account or spin up your own instance of MakinForU. 1. **[Install MakinForU](/docs/get-started/install-makinforu)** - Add the script tag or use one of our SDKs 2. **[Track Events](/docs/get-started/track-events)** - Start measuring user actions 3. **[Identify Users](/docs/get-started/identify-users)** - Connect events to specific users 4. **[Track Revenue](/docs/revenue-tracking)** - Monitor purchases and subscriptions πŸ”’ Privacy First [#-privacy-first] MakinForU is built with privacy in mind: * **No cookies required** - Cookieless tracking by default * **GDPR and CCPA compliant** - Built for privacy regulations * **Self-hosting option** - Full control over your data * **Transparent data handling** - You own your data 🌐 Open Source [#-open-source] MakinForU is fully open-source and available on [GitHub](https://github.com/deviljoker1911-beep/makinforu-panel). We believe in transparency and community-driven development. πŸ’¬ Need Help? [#-need-help] * Join our [GitHub Discussions](https://github.com/deviljoker1911-beep/makinforu-panel/discussions) * Check our [GitHub issues](https://github.com/deviljoker1911-beep/makinforu-panel/issues) * Email us at [hello@makinforu.com](mailto:hello@makinforu.com) --- ## Avoid adblockers with proxy URL: https://panel.makinforu.com/docs/adblockers In this article we need to talk about adblockers, why they exist, how they work, and how to avoid them. Adblockers' main purpose was initially to block ads, but they have since started to block tracking scripts as well. This is primarily for privacy reasons, and while we respect that, there are legitimate use cases for understanding your visitors. MakinForU is designed to be a privacy-friendly, cookieless analytics tool that doesn't track users across sites, but generic blocklists often catch all analytics tools indiscriminately. The best way to avoid adblockers is to proxy events via your own domain name. Adblockers generally cannot block requests to your own domain (first-party requests) without breaking the functionality of the site itself. Built-in Support [#built-in-support] Today, our Next.js SDK and WordPress plugin have built-in support for proxying: * **WordPress**: Does it automatically. * **Next.js**: Easy to setup with a route handler. Implementing Proxying for Any Framework [#implementing-proxying-for-any-framework] If you are not using Next.js or WordPress, you can implement proxying in any backend framework. The key is to set up an API endpoint on your domain (e.g., `api.domain.com` or `domain.com/api`) that forwards requests to MakinForU. Below is an example of how to set up a proxy using a [Hono](https://hono.dev/) server. This implementation mimics the logic used in our Next.js SDK. > You can always see how our Next.js implementation looks like in our [repository](https://github.com/deviljoker1911-beep/makinforu-panel/blob/main/packages/sdks/nextjs/createNextRouteHandler.ts). Hono Example [#hono-example] ```typescript import { Hono } from 'hono' const app = new Hono() // 1. Proxy the script file app.get('/op1.js', async (c) => { const scriptUrl = 'https://panel.makinforu.com/op1.js' try { const res = await fetch(scriptUrl) const text = await res.text() c.header('Content-Type', 'text/javascript') // Optional caching for 24 hours c.header('Cache-Control', 'public, max-age=86400, stale-while-revalidate=86400') return c.body(text) } catch (e) { return c.json({ error: 'Failed to fetch script' }, 500) } }) // 2. Proxy the track event app.post('/track', async (c) => { const body = await c.req.json() // Forward the client's IP address (be sure to pick correct IP based on your infra) const ip = c.req.header('cf-connecting-ip') ?? c.req.header('x-forwarded-for')?.split(',')[0] const headers = new Headers() headers.set('Content-Type', 'application/json') headers.set('Origin', c.req.header('origin') ?? '') headers.set('User-Agent', c.req.header('user-agent') ?? '') headers.set('makinforu-client-id', c.req.header('makinforu-client-id') ?? '') if (ip) { headers.set('makinforu-client-ip', ip) } try { const res = await fetch('https://api.makinforu.com/track', { method: 'POST', headers, body: JSON.stringify(body), }) return c.json(await res.text(), res.status) } catch (e) { return c.json(e, 500) } }) export default app ``` This script sets up two endpoints: 1. `GET /op1.js`: Fetches the MakinForU script and serves it from your domain. 2. `POST /track`: Receives events from the frontend, adds necessary headers (User-Agent, Origin, Content-Type, makinforu-client-id, makinforu-client-ip), and forwards them to MakinForU's API. Frontend Configuration [#frontend-configuration] Once your proxy is running, you need to configure the MakinForU script on your frontend to use your proxy endpoints instead of the default ones. ```html ``` By doing this, all requests are sent to your domain first, bypassing adblockers that look for third-party tracking domains. --- ## Authentication URL: https://panel.makinforu.com/docs/api-reference/authentication Client ID & Secret [#client-id--secret] MakinForU uses client credentials for authentication. Every API client has a **Client ID** and a **Client Secret** that you generate from the dashboard. Pass them as request headers: ```http makinforu-client-id: makinforu-client-secret: ``` API Client Types [#api-client-types] | Type | Description | | ------- | ------------------------------------------------ | | `write` | Can ingest events and profile updates | | `read` | Can query analytics data (insights, export, etc) | | `root` | Full access β€” use only for server-side admin | Creating a Client [#creating-a-client] Go to **Settings β†’ API Clients** in your dashboard and create a client with the appropriate type. Copy the secret immediately β€” it is only shown once. For more details on authentication, permissions, and security best practices, see the [Authentication guide](/docs/api/authentication). --- ## Rate Limits URL: https://panel.makinforu.com/docs/api-reference/rate-limits Limits [#limits] | Endpoint group | Limit | | -------------- | -------------------- | | Insights | 100 req / 10 seconds | | Export | 100 req / 10 seconds | | Manage | 20 req / 10 seconds | Limits are applied per **Client ID**. Track, Profile, and Import endpoints do not have a rate limit applied. Handling 429s [#handling-429s] When you exceed the limit the API returns `429 Too Many Requests`: ```json { "status": 429, "error": "Too Many Requests", "message": "You have exceeded the rate limit for this endpoint." } ``` Wait for the rate limit window to reset before retrying. --- ## Authentication URL: https://panel.makinforu.com/docs/api/authentication Authentication [#authentication] To authenticate with the MakinForU API, you need to use your `clientId` and `clientSecret`. Different API endpoints may require different access levels: * **Track API**: Default client works with `write` mode * **Export API**: Requires `read` or `root` mode * **Insights API**: Requires `read` or `root` mode * **Manage API**: Requires `root` mode only The default client (created with a project) has `write` mode and does not have access to the Export, Insights, or Manage APIs. You'll need to create additional clients with appropriate access levels. Headers [#headers] Include the following headers with your API requests: * `makinforu-client-id`: Your MakinForU client ID * `makinforu-client-secret`: Your MakinForU client secret Example [#example] ```bash curl 'https://api.makinforu.com/insights/{projectId}/metrics' \ -H 'makinforu-client-id: YOUR_CLIENT_ID' \ -H 'makinforu-client-secret: YOUR_CLIENT_SECRET' ``` Security Best Practices [#security-best-practices] 1. **Store credentials securely**: Never expose your `clientId` and `clientSecret` in client-side code 2. **Use HTTPS**: Always use HTTPS to ensure secure communication 3. **Rotate credentials**: Regularly rotate your API credentials 4. **Limit access**: Use the minimum required access level for your use case Error Responses [#error-responses] If authentication fails, you'll receive a `401 Unauthorized` response: ```json { "error": "Unauthorized", "message": "Invalid client credentials" } ``` Common authentication errors: * Invalid client ID or secret * Client doesn't have required permissions (e.g., trying to access Manage API with a non-root client) * Malformed client ID (must be a valid UUIDv4) * Client type mismatch (e.g., `write` client trying to access Export API) Client Types [#client-types] MakinForU supports three client types with different access levels: | Type | Description | Access | | ------- | ---------------- | ----------------------------- | | `write` | Write access | Track API only | | `read` | Read-only access | Export API, Insights API | | `root` | Full access | All APIs including Manage API | **Note**: Root clients have organization-wide access and can manage all resources. Use root clients carefully and store their credentials securely. Rate Limiting [#rate-limiting] The API implements rate limiting to prevent abuse. Rate limits vary by endpoint: * **Track API**: Higher limits for [event tracking](/features/event-tracking) * **Export/Insights APIs**: 100 requests per 10 seconds * **Manage API**: 20 requests per 10 seconds If you exceed the rate limit, you'll receive a `429 Too Many Requests` response. Implement exponential backoff for retries. Remember to replace `YOUR_CLIENT_ID` and `YOUR_CLIENT_SECRET` with your actual MakinForU API credentials. --- ## Export URL: https://panel.makinforu.com/docs/api/export Authentication [#authentication] Requires a `read` or `root` client β€” the default `write` client does not have access. See the [Authentication](/docs/api/authentication) guide. Endpoints [#endpoints] | Endpoint | Description | | -------------------- | ---------------------------------------------------- | | `GET /export/events` | Paginated list of raw events with optional filtering | | `GET /export/charts` | Aggregated time-series data with breakdowns | Filtering events [#filtering-events] The `/export/events` endpoint accepts filters, pagination, and an `includes` parameter to attach related data (profile, meta, properties, geo, device, referrer). Chart series [#chart-series] The `/export/charts` endpoint accepts a `series` array where each item can specify an event name, filters, and a segment type (`event`, `user`, `session`, `property_sum`, etc.). For full query parameter and response schemas, see the [API Reference](/docs/api-reference). --- ## Insights URL: https://panel.makinforu.com/docs/api/insights Authentication [#authentication] Requires a `read` or `root` client β€” the default `write` client does not have access. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.makinforu.com/insights/{projectId} ``` Available endpoints [#available-endpoints] | Endpoint | Description | | ------------------------------------- | ----------------------------------------------- | | `GET /metrics` | Visitors, sessions, bounce rate, and engagement | | `GET /live` | Current active visitor count | | `GET /pages` | Top pages by sessions | | `GET /referrer` | Traffic sources | | `GET /country`, `/region`, `/city` | Geographic breakdown | | `GET /device`, `/browser`, `/os` | Device and technology breakdown | | `GET /utm_source`, `/utm_campaign`, … | UTM parameter breakdown | Most endpoints accept `startDate`, `endDate`, `range`, `filters`, `cursor`, and `limit` query parameters. For full schemas and all available endpoints, see the [API Reference](/docs/api-reference). --- ## Manage API Overview URL: https://panel.makinforu.com/docs/api/manage Authentication [#authentication] The Manage API requires a **root** client. Root clients have organization-wide access and can manage all resources. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.makinforu.com/manage ``` Resources [#resources] | Resource | Description | | ---------- | ---------------------------------------------------------- | | Projects | Create, update, and delete analytics projects | | Clients | Manage API clients (read / write / root) and their secrets | | References | Mark important dates or events on your analytics timeline | Rate limiting [#rate-limiting] 20 requests per 10 seconds per client. For full endpoint schemas, see the [API Reference](/docs/api-reference). --- ## Clients URL: https://panel.makinforu.com/docs/api/manage/clients Authentication [#authentication] Requires a `root` client. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.makinforu.com/manage/clients ``` Client types [#client-types] | Type | Description | | ------- | ----------------------------------------- | | `write` | Ingest events and profile updates | | `read` | Query analytics data (insights, export) | | `root` | Full access β€” manage API, read, and write | Endpoints [#endpoints] | Method | Path | Description | | -------- | ---------------------- | --------------------------------------------------- | | `GET` | `/manage/clients` | List all clients (optionally filter by `projectId`) | | `GET` | `/manage/clients/{id}` | Get a specific client | | `POST` | `/manage/clients` | Create a new client | | `PATCH` | `/manage/clients/{id}` | Update client name | | `DELETE` | `/manage/clients/{id}` | Permanently delete a client | Client secrets are only returned once at creation time and are never retrievable afterwards. For full request/response schemas, see the [API Reference](/docs/api-reference). --- ## Projects URL: https://panel.makinforu.com/docs/api/manage/projects Authentication [#authentication] Requires a `root` client. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.makinforu.com/manage/projects ``` Endpoints [#endpoints] | Method | Path | Description | | -------- | ----------------------- | ---------------------------------------- | | `GET` | `/manage/projects` | List all projects in your organization | | `GET` | `/manage/projects/{id}` | Get a specific project | | `POST` | `/manage/projects` | Create a new project | | `PATCH` | `/manage/projects/{id}` | Update a project | | `DELETE` | `/manage/projects/{id}` | Soft-delete a project (24h grace period) | When you create a project, a default `write` client is automatically created and returned with the response. The client secret is only shown once. For full request/response schemas, see the [API Reference](/docs/api-reference). --- ## References URL: https://panel.makinforu.com/docs/api/manage/references Authentication [#authentication] Requires a `root` client. See the [Authentication](/docs/api/authentication) guide. Base URL [#base-url] ``` https://api.makinforu.com/manage/references ``` What are references? [#what-are-references] References are markers on your analytics timeline β€” useful for product launches, campaign start dates, feature releases, or any event you want to correlate with changes in your metrics. Endpoints [#endpoints] | Method | Path | Description | | -------- | ------------------------- | ------------------------------------------------------ | | `GET` | `/manage/references` | List all references (optionally filter by `projectId`) | | `GET` | `/manage/references/{id}` | Get a specific reference | | `POST` | `/manage/references` | Create a new reference | | `PATCH` | `/manage/references/{id}` | Update a reference | | `DELETE` | `/manage/references/{id}` | Delete a reference | For full request/response schemas, see the [API Reference](/docs/api-reference). --- ## Track URL: https://panel.makinforu.com/docs/api/track Good to know [#good-to-know] * Pass the `x-client-ip` header to enable geo location tracking * Pass the `user-agent` header to enable device detection Authentication [#authentication] All requests require a `write` or `root` client. See the [Authentication](/docs/api/authentication) guide. ```bash -H "makinforu-client-id: YOUR_CLIENT_ID" \ -H "makinforu-client-secret: YOUR_CLIENT_SECRET" ``` Base URL [#base-url] ``` https://api.makinforu.com ``` Event types [#event-types] The `/track` endpoint accepts a `type` field that determines what gets recorded: | Type | Description | | -------------- | --------------------------------------------- | | `track` | Record a named event with optional properties | | `identify` | Create or update a user profile | | `increment` | Increment a numeric profile property | | `decrement` | Decrement a numeric profile property | | `group` | Create or update a group | | `assign_group` | Link a profile to one or more groups | Groups and events [#groups-and-events] Groups are never auto-populated on events β€” even after `assign_group`. Pass `groups` explicitly on each `track` call where you need group data. For full request/response schemas for every event type, see the [API Reference](/docs/api-reference/track/track/post). --- ## Consent management URL: https://panel.makinforu.com/docs/consent-management Some jurisdictions require explicit user consent before you can track events or record sessions. MakinForU has built-in support for this: initialise with `disabled: true` and nothing is sent until you call `ready()`. How it works [#how-it-works] When `disabled: true` is set, all calls to `track`, `identify`, `screenView`, and session replay chunks are held in an in-memory queue instead of being sent to the API. Once the user consents, call `ready()` and the entire queue is flushed immediately. ```ts const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', disabled: true, // nothing sent until ready() is called }); // Later, when the user accepts your consent banner: op.ready(); ``` If the user declines, simply don't call `ready()`. The queue is discarded when the page unloads. With session replay [#with-session-replay] Session replay chunks are also queued while `disabled: true`. Once `ready()` is called, buffered replay chunks flush along with any queued events. ```ts const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', disabled: true, trackScreenViews: true, sessionReplay: { enabled: true }, }); // User accepts consent: op.ready(); ``` The replay recorder starts as soon as the page loads (so no interactions are missed), but no data is sent until `ready()` is called. Waiting for a user profile [#waiting-for-a-user-profile] If you want to hold events until you know who the user is rather than waiting for explicit consent, use `waitForProfile` instead. Events are queued until `identify()` is called with a `profileId`. ```ts const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', waitForProfile: true, }); // Events queue here... op.track('page_view'); // Queue is flushed once a profileId is set: op.identify({ profileId: 'user_123' }); ``` If the user never authenticates, the queue is never flushed automatically β€” no events will be sent. To handle anonymous users or guest flows, call `ready()` explicitly when you know the user won't identify: ```ts // User skipped login β€” flush queued events without a profileId op.ready(); ``` `ready()` always releases the queue regardless of whether `waitForProfile` or `disabled` is set. Related [#related] * [Consent management guide](/guides/consent-management) β€” full walkthrough with a cookie banner example * [Session replay](/docs/session-replay) β€” privacy controls for replay recordings * [Identify users](/docs/get-started/identify-users) β€” link events to a user profile --- ## How to set up notifications and integrations URL: https://panel.makinforu.com/docs/dashboard/notifications-and-integrations How it works [#how-it-works] There are two separate concepts to understand before you start: * **Integrations** are connections to external services like Slack, Discord, or a custom webhook. They live at the workspace/organization level and can be reused across all your projects. * **Notification rules** are the conditions that trigger a notification. Rules live inside individual projects and reference one or more integrations. A rule does nothing until it has an integration attachedβ€”and an integration does nothing until a rule uses it. * **Notifications** are the messages that are sent when a rule is triggered. A notification can be sent as a json object or a template with variables. Step 1: Create an integration [#create-integration] Go to your workspace settings and open the **Integrations** section. Click **Add integration** and choose the service you want to connect. MakinForU currently supports: * **Slack** β€” authenticate via OAuth and pick a channel * **Discord** β€” paste a Discord webhook URL for a channel * **Webhook** β€” send an HTTP POST to any URL you control Fill in the required details and save. The integration is now available to all projects in your workspace.
Soon we have integrations for S3 and GCS to export your events to your own storage. Step 2: Go to your project's notification rules [#create-rule] Integrations alone don't do anything. To start receiving alerts, open the project you want to monitor, click **Notifications** in the left sidebar, and switch to the **Rules** tab. Click **Add Rule** to open the rule editor on the right side of the screen. Give your rule a name, then choose a **Type**. There are two types: | Type | When it triggers | | ---------- | ---------------------------------------------------------------------- | | **Event** | Immediately when a matching event is received | | **Funnel** | After a session ends and all funnel steps have been completed in order | Event rules [#event-rules] Event rules fire in real time. The moment MakinForU receives an event that matches your filters, the notification is sent.
In the rule editor: 1. Set **Type** to **Events** 2. Add one or more events from the **Events** list. You can filter each event by its properties (for example, only trigger when `path` starts with `/onboarding`) 3. Write a **Template** for the notification message. Use `{{property_name}}` to insert event properties dynamicallyβ€”for example, `New user with their first event from {{country}}`. 4. Under **Integrations**, select which integration(s) should receive the notification Click **Update** to save the rule. Templates will not be used if you have Javascript transformer in your integration. Funnel rules [#funnel-rules] Funnel rules let you track multi-step flows and notify you only when a user completes every step in the correct sequenceβ€”for example, `session_start` β†’ `subscription_checkout` β†’ `subscription_created`.
In the rule editor: 1. Set **Type** to **Funnel** 2. Add each event in the funnel, in the order they must occur. You can optionally add property filters to each step 3. Write a **Template** for the notification message 4. Select your **Integration(s)** Click **Update** to save. **Important:** Funnel rule notifications are sent after the session ends, not immediately when the last step fires. MakinForU waits until the session is complete before evaluating the funnel sequence. Templates will not be used if you have Javascript transformer in your integration. View notifications [#view-notifications] Switch to the **Notifications** tab (the default view) to see every notification that has been triggered for your project. Each row shows the notification title alongside the country, OS, browser, and profile of the user who triggered it.
You can filter the list by creation date or search by title to find specific events. Frequently asked questions [#frequently-asked-questions] Yes. Integrations are created at the workspace level, so any project in your organization can reference them in its notification rules. Funnel rules trigger after the session ends, not when the last event fires. If the user's session is still active, the notification is queued until the session closes. Make sure the full funnel sequence was completed within a single session. Yes. For each event in the rule, click the filter icon to add property conditionsβ€”for example, only trigger when `plan` equals `enterprise` or `country` equals `US`. Currently Slack, Discord, and custom webhooks. More integrations are coming soon. Yes. The integrations selector on each rule allows you to pick multiple destinations. A single triggered rule will send a notification to all selected integrations simultaneously. --- ## Understand the overview URL: https://panel.makinforu.com/docs/dashboard/understand-the-overview Top stats [#top-stats] The row of metric cards at the top of the page is the fastest way to understand the health of your project. Each card shows the value for the selected time range and a comparison to the previous period of the same length. Unique Visitors [#unique-visitors] The number of distinct profile IDs recorded in the selected period. How accurate this is depends on whether you use [identify](/docs/get-started/identify-users): * **Without identify**: MakinForU generates an anonymous profile ID that rotates every 24 hours. A visitor returning on 10 different days will be counted as 10 unique visitors, because each day produces a new ID. * **With identify**: The profile ID is tied to the user's real identity. The same person visiting on 10 different days is counted as 1 unique visitor across the entire period. If cross-day deduplication matters to your analysis, set up [user identification](/docs/get-started/identify-users). Sessions [#sessions] The total number of sessions in the selected period. A session begins when someone arrives on your site and ends after 30 minutes of inactivity or when they close the tab. One visitor can have many sessions across a day. Pageviews [#pageviews] The total number of page views (`screen_view` events) recorded across all sessions. Every time a visitor loads a pageβ€”including navigating between pages in a single sessionβ€”it counts as one pageview. Pages per Session [#pages-per-session] The average number of pages viewed within a single session, calculated as `total pageviews / total sessions`. A higher number means visitors are exploring more of your site before leaving. Bounce Rate [#bounce-rate] The percentage of sessions where a visitor viewed only a single page and left. Calculated as `single-page sessions / total sessions Γ— 100`. Lower is generally betterβ€”it means more visitors are engaging beyond the first page. > A session is counted as a bounce if the visitor triggered exactly one `screen_view` event before the session ended. Sessions where visitors read one article deeply and leave still count as bounces. Session Duration [#session-duration] The average length of a session in seconds, calculated only from sessions where the visitor did something after the first page load (duration > 0). Sessions where a visitor immediately left are excluded from the average to avoid skewing the number. Revenue [#revenue] The total monetary value tracked via `revenue` events in the selected period, displayed in your account currency. Revenue is only shown if you are tracking revenue events. See the [revenue tracking docs](/features/revenue-tracking) for setup instructions. *** The time-series chart [#the-time-series-chart] Directly below the stat cards is a line chart that shows how the selected metric changes over time. Click any stat card to switch the chart to that metric. The chart uses the **interval** you select (hour, day, week, or month) to group data points. A faint dashed line shows the equivalent period from the previous comparison window, so you can spot trends at a glance. When any metric other than Revenue is active, the chart also overlays revenue as green bars on a secondary Y-axisβ€”this lets you correlate traffic patterns with revenue without switching cards. The trailing edge of the line (the current, incomplete interval) is shown as a dashed segment to remind you that the period is still accumulating data. *** Insights [#insights] A scrollable row of insight cards appears below the chart once your project has at least 30 days of data. MakinForU automatically detects significant trends across pageviews, entry pages, referrers, and countriesβ€”no configuration needed. Each card shows: * **Share**: The percentage of total traffic that property represents (e.g., "United States: 42% of all sessions") * **Absolute change**: The raw increase or decrease in sessions compared to the previous period * **Percentage change**: How much that property grew or declined relative to its own previous value For example, if the US had 1,000 sessions last week and 1,200 this week, the card shows "+200 sessions (+20%)". Clicking any insight card filters the entire overview page to show only data matching that propertyβ€”letting you drill into what's driving the trend. *** Sources [#sources] The Sources widget shows where your visitors came from. Switch between tabs to see different dimensions: | Tab | What it shows | | ------------ | ----------------------------------------------------------------- | | **Refs** | Grouped referrer names (e.g., "Google", "Twitter", "Hacker News") | | **Urls** | Raw referrer URLs | | **Types** | Referrer categories: `search`, `social`, `email`, `unknown` | | **Source** | `utm_source` query parameter values | | **Medium** | `utm_medium` query parameter values | | **Campaign** | `utm_campaign` query parameter values | | **Term** | `utm_term` query parameter values | | **Content** | `utm_content` query parameter values | Referrer names and types are resolved automatically from the raw referrer URL using a built-in lookup table. Direct traffic (no referrer) appears as `(not set)`. Each row shows sessions and pageviews. Clicking a row filters the entire overview page to only show data from that source. *** Pages [#pages] The Pages widget shows which URLs your visitors are landing on, exiting from, and spending time on. | Tab | What it shows | | --------------- | --------------------------------------------------------------------------- | | **Top pages** | Pages ranked by unique sessions. Each row is a `origin + path` combination. | | **Entry pages** | The first page of each sessionβ€”the page where visitors arrived. | | **Exit pages** | The last page of each sessionβ€”the page where visitors left. | High exit rates on a page are not always badβ€”they can reflect a page that successfully answers a question. High bounce on an entry page is more diagnostic. Compare entry and exit distributions to understand the shape of your user journeys. Clicking a page row filters the whole overview to sessions that included that page. *** Devices [#devices] The Devices widget breaks down your audience by hardware and software. Switch between tabs: | Tab | What it shows | | ---------------- | ----------------------------------------------------- | | **Device** | Device type: Desktop, Mobile, Tablet | | **Brand** | Hardware brand (Apple, Samsung, etc.) | | **Model** | Specific device model | | **Browser** | Browser name (Chrome, Safari, Firefox, etc.) | | **Browser ver.** | Browser version number | | **OS** | Operating system (macOS, Windows, iOS, Android, etc.) | | **OS ver.** | Operating system version | Each row shows sessions and pageviews. Use this widget to prioritize which browsers and operating systems to test and optimize for. *** Events [#events] The Events widget shows the most frequent custom events fired in the selected period, ranked by count. System events (`session_start`, `session_end`, `screen_view`) are excludedβ€”only the events you instrument yourself appear here. Click any event to filter the overview to sessions where that event was fired. *** Geo [#geo] The Geo widget shows the geographic distribution of your visitors. Switch between tabs: | Tab | What it shows | | ----------- | -------------------------------------------- | | **Country** | Visitor country, derived from IP geolocation | | **Region** | State or province | | **City** | City level | Below the table, a world map plots the same data as a heatmapβ€”darker areas represent more sessions. This gives you a quick visual of where your audience is concentrated. Clicking a country, region, or city filters the whole overview to that location. *** Activity heatmap [#activity-heatmap] The activity heatmap at the bottom of the page shows when your visitors are most active, broken down by day of the week (Monday through Sunday) and hour of the day (00:00–23:00). Each cell shows the **average** of the selected metric at that day-and-hour combination, averaged across all weeks in the selected period. Darker cells indicate higher average values. Hover any cell to see the exact average. You can switch the metric being visualized using the tabs above the heatmap: * **Unique Visitors** * **Sessions** * **Pageviews** * **Bounce Rate** * **Pages / Session** * **Session Duration** Use the heatmap to identify peak traffic windows, plan campaigns, and schedule maintenance during quiet periods. *** User Journey [#user-journey] The User Journey (Sankey) diagram at the very bottom visualizes how visitors flow through your site within a session. It answers the question: after landing on page A, where do visitors go next? **How it works:** 1. MakinForU identifies the top 3 most common entry pages in the selected period. 2. From each entry page, it finds the top 3 most frequent next pages (step 2), then the top 3 from those (step 3), and so on up to the configured number of steps (default 5, adjustable to a maximum of 10). 3. Paths that represent less than 0.25% of total sessions are filtered out to reduce visual noise. 4. Consecutive duplicate pages within a session are collapsed into one step (e.g., if someone refreshed a page, it only counts once in the journey). Each node shows the page URL. The width of the connecting flows is proportional to the number of sessions that followed that path. Use the User Journey to find drop-off points, discover unexpected popular paths, and understand whether visitors are reaching your key conversion pages. *** Filters and time controls [#filters-and-time-controls] Every widget on the overview page responds to the same set of global filters and time controls at the top of the page. **Range**: choose a preset (Today, Last 7 days, Last 30 days, etc.) or a custom date range. **Interval**: controls how data is grouped in the time-series chart (hour, day, week, month). **Event filter**: narrow the entire overview to sessions that include a specific eventβ€”useful for analyzing the behavior of users who completed a particular action. **Dimension filters**: clicking any row in any widget (a country, a source, a page) applies that value as a filter. Active filters are shown as chips below the time controls. Remove a filter by clicking the Γ— on its chip. **Live counter**: a green badge in the top-right corner shows the number of active visitors (visitors who fired an event in the last 5 minutes). Click it for a 30-minute session histogram. --- ## Groups URL: https://panel.makinforu.com/docs/get-started/groups Groups let you associate users with a shared entity β€” like a company, workspace, or team β€” and analyze behavior at that level. Instead of asking "what did Jane do?", you can ask "what is Acme Inc doing?" This is especially useful for B2B SaaS products where a single paying account has many users. How Groups work [#how-groups-work] There are two separate concepts: 1. **The group entity** β€” created/updated with `upsertGroup()`. Stores metadata about the group (name, plan, etc.). 2. **Group membership** β€” set with `setGroup()` / `setGroups()`. Links a user profile to one or more groups, and automatically attaches those group IDs to every subsequent `track()` call. Creating or updating a group [#creating-or-updating-a-group] Call `upsertGroup()` to create a group or update its properties. The group is identified by its `id` and `type`. ```typescript op.upsertGroup({ id: 'org_acme', // Your group's unique ID type: 'company', // Group type (company, workspace, team, etc.) name: 'Acme Inc', // Display name properties: { plan: 'enterprise', seats: 25, industry: 'logistics', }, }); ``` Group payload [#group-payload] | Field | Type | Required | Description | | ------------ | -------- | -------- | --------------------------------------------------- | | `id` | `string` | Yes | Unique identifier for the group | | `type` | `string` | Yes | Category of group (e.g. `"company"`, `"workspace"`) | | `name` | `string` | Yes | Human-readable display name | | `properties` | `object` | No | Custom metadata about the group | Managing groups in the dashboard [#managing-groups-in-the-dashboard] The easiest way to create, edit, and delete groups is directly in the MakinForU dashboard. Navigate to your project and open the **Groups** section β€” from there you can manage group names, types, and properties without touching any code. `upsertGroup()` is the right tool when your group properties are **dynamic and driven by your own data** β€” for example, syncing a customer's current plan, seat count, or MRR from your backend at login time. A good rule of thumb: call `upsertGroup()` on login or when group properties change β€” not on every request or page view. If you find yourself calling it frequently with the same data, the dashboard is probably the better place to manage that group. Assigning a user to a group [#assigning-a-user-to-a-group] After identifying a user, call `setGroup()` to link them to a group. This also attaches the group ID to all future `track()` calls for the current session. ```typescript // After login op.identify({ profileId: 'user_123' }); // Link the user to their organization op.setGroup('org_acme'); ``` For users that belong to multiple groups: ```typescript op.setGroups(['org_acme', 'team_engineering']); ``` `setGroup()` and `setGroups()` persist group IDs on the SDK instance. All subsequent `track()` calls will automatically include these group IDs until `clear()` is called. Full login flow example [#full-login-flow-example] `setGroup()` doesn't require the group to exist first. You can call it with just an ID β€” events will be tagged with that group ID, and you can create the group later in the dashboard or via `upsertGroup()`. ```typescript // 1. Identify the user op.identify({ profileId: 'user_123', firstName: 'Jane', email: 'jane@acme.com', }); // 2. Assign the user to the group β€” the group doesn't need to exist yet op.setGroup('org_acme'); // 3. All subsequent events are now tagged with the group op.track('dashboard_viewed'); // β†’ includes groups: ['org_acme'] op.track('report_exported'); // β†’ includes groups: ['org_acme'] ``` If you want to sync dynamic group properties from your own data (plan, seats, MRR), add `upsertGroup()` to the flow: ```typescript op.identify({ profileId: 'user_123', email: 'jane@acme.com' }); // Sync group metadata from your backend op.upsertGroup({ id: 'org_acme', type: 'company', name: 'Acme Inc', properties: { plan: 'pro' }, }); op.setGroup('org_acme'); ``` Per-event group override [#per-event-group-override] You can attach group IDs to a specific event without affecting the SDK's persistent group state: ```typescript op.track('file_shared', { filename: 'q4-report.pdf', groups: ['org_acme', 'org_partner'], // Only applies to this event }); ``` Groups passed in `track()` are **merged** with any groups already set on the SDK instance. Clearing groups on logout [#clearing-groups-on-logout] `clear()` resets the profile, device, session, and all groups. Always call it on logout. ```typescript function handleLogout() { op.clear(); // redirect to login... } ``` Common patterns [#common-patterns] B2B SaaS β€” company accounts [#b2b-saas--company-accounts] ```typescript // On login op.identify({ profileId: user.id, email: user.email }); op.upsertGroup({ id: user.organizationId, type: 'company', name: user.organizationName, properties: { plan: user.plan, mrr: user.mrr }, }); op.setGroup(user.organizationId); ``` Multi-tenant β€” workspaces [#multi-tenant--workspaces] ```typescript // When user switches workspace op.upsertGroup({ id: workspace.id, type: 'workspace', name: workspace.name, }); op.setGroup(workspace.id); ``` Teams within a company [#teams-within-a-company] ```typescript // User belongs to a company and a specific team op.setGroups([user.organizationId, user.teamId]); ``` API reference [#api-reference] `upsertGroup(payload)` [#upsertgrouppayload] Creates the group if it doesn't exist, or merges properties into the existing group. ```typescript op.upsertGroup({ id: string; // Required type: string; // Required name: string; // Required properties?: Record; }); ``` `setGroup(groupId)` [#setgroupgroupid] Adds a single group ID to the SDK's internal group list and sends an `assign_group` event to link the current profile to that group. ```typescript op.setGroup('org_acme'); ``` `setGroups(groupIds)` [#setgroupsgroupids] Same as `setGroup()` but for multiple group IDs at once. ```typescript op.setGroups(['org_acme', 'team_engineering']); ``` What to avoid [#what-to-avoid] * **Calling `upsertGroup()` on every event or page view** β€” call it on login or when group properties actually change. For static group management, use the dashboard instead. * **Not calling `setGroup()` after `identify()`** β€” without it, events won't be tagged with the group and you won't see group-level data in the dashboard. * **Forgetting `clear()` on logout** β€” groups persist on the SDK instance, so a new user logging in on the same session could inherit the previous user's groups. * **Using `upsertGroup()` to link a user to a group** β€” `upsertGroup()` manages the group entity only. Use `setGroup()` to link a user profile to it. --- ## Identify Users URL: https://panel.makinforu.com/docs/get-started/identify-users By default, MakinForU tracks visitors anonymously. To connect these events to a specific user in your database, you need to identify them. How it works [#how-it-works] When a user logs in or signs up, you should call the `identify` method. This associates their current session and all future events with their unique ID from your system. ```javascript op.identify({ profileId: 'user_123' }); ``` Adding user traits [#adding-user-traits] You can also pass user traits (like name, email, or plan type) when you identify them. These traits will appear in the user's profile in your dashboard. ```javascript op.identify({ profileId: 'user_123', firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com', company: 'Acme Inc' }); ``` Standard traits [#standard-traits] We recommend using these standard keys for common user information so they display correctly in the MakinForU dashboard: * `firstName` * `lastName` * `email` * `phone` * `avatar` Best Practices [#best-practices] 1. **Call on login**: Always identify the user immediately after they log in. 2. **Call on update**: If a user updates their profile, call identify again with the new information. 3. **Unique IDs**: Use a stable, unique ID from your database (like a UUID) rather than an email address or username that might change. --- ## Install MakinForU URL: https://panel.makinforu.com/docs/get-started/install-makinforu The quickest way to get started with MakinForU is to use our Web SDK. It works with any website. Quick Start [#quick-start] Simply add this script tag to your website's `` section. ```html title="index.html" ``` That's it! MakinForU will now automatically track: * Page views * Visit duration * Referrers * Device and browser information * Location Using a Framework? [#using-a-framework] If you are using a specific framework or platform, we have dedicated SDKs that provide a better developer experience. Explore all SDKs [#explore-all-sdks] We support many more platforms. Check out our [SDKs Overview](/docs/sdks) for the full list. --- ## Track Events URL: https://panel.makinforu.com/docs/get-started/track-events Events are the core of MakinForU. They allow you to measure specific actions users take on your site, like clicking a button, submitting a form, or completing a purchase. Tracking an event [#tracking-an-event] To track an event, simply call the `track` method with an event name. ```javascript op.track('button_clicked'); ``` Adding properties [#adding-properties] You can add additional context to your events by passing a properties object. This helps you understand the details of the interaction. ```javascript op.track('signup_button_clicked', { location: 'header', color: 'blue', variant: 'primary' }); ``` Common property types [#common-property-types] * **Strings**: Text values like names, categories, or IDs. * **Numbers**: Numeric values like price, quantity, or score. * **Booleans**: True or false values. Using Data Attributes [#using-data-attributes] If you prefer not to write JavaScript, you can use data attributes to track clicks automatically. ```html ``` When a user clicks this button, MakinForU will automatically track a `signup_clicked` event with the property `location: 'header'`. --- ## How it works URL: https://panel.makinforu.com/docs/how-it-works Device ID [#device-id] A **device ID** is a unique identifier generated for each device/browser combination. It's calculated using a hash function that combines: * **User Agent** (browser/client information) * **IP Address** * **Origin** (project ID) * **Salt** (a rotating secret key) ```typescript export function generateDeviceId({ salt, ua, ip, origin, }: GenerateDeviceIdOptions) { return createHash(`${ua}:${ip}:${origin}:${salt}`, 16); } ``` Salt Rotation [#salt-rotation] The salt used for device ID generation rotates **daily at midnight** (UTC). This means: * Device IDs remain consistent throughout a single day * Device IDs reset each day for privacy purposes * The system maintains both the current and previous day's salt to handle events that may arrive slightly after midnight ```typescript // Salt rotation happens daily at midnight (pattern: '0 0 * * *') ``` When the salt rotates, all device IDs change, effectively anonymizing tracking data on a daily basis while still allowing session continuity within a 24-hour period. Session ID [#session-id] A **session** represents a continuous period of user activity. Sessions are used to group related events together and understand user behavior patterns. Session Duration [#session-duration] Sessions have a **30-minute timeout**. If no events are received for 30 minutes, the session automatically ends. Each new event resets this 30-minute timer. ```typescript export const SESSION_TIMEOUT = 1000 * 60 * 30; // 30 minutes ``` Session Creation Rules [#session-creation-rules] Sessions are **only created for client events**, not server events. This means: * Events sent from browsers, mobile apps, or client-side SDKs will create sessions * Events sent from backend servers, scripts, or server-side SDKs will **not** create sessions * If you only track events from your backend, no sessions will be created Additionally, sessions are **not created for events older than 15 minutes**. This prevents historical data imports from creating artificial sessions. ```typescript // Sessions are not created if: // 1. The event is from a server (uaInfo.isServer === true) // 2. The timestamp is from the past (isTimestampFromThePast === true) if (uaInfo.isServer || isTimestampFromThePast) { // Event is attached to existing session or no session } ``` Profile ID [#profile-id] A **profile ID** is a persistent identifier for a user across multiple devices and sessions. It allows you to track the same user across different browsers, devices, and time periods. Profile ID Assignment [#profile-id-assignment] If a `profileId` is provided when tracking an event, it will be used to identify the user. However, **if no `profileId` is provided, it defaults to the `deviceId`**. This means: * Anonymous users (without a profile ID) are tracked by their device ID * Once you identify a user (by providing a profile ID), all their events will be associated with that profile * The same user can be tracked across multiple devices by using the same profile ID ```typescript // If no profileId is provided, it defaults to deviceId if (!payload.profileId && payload.deviceId) { payload.profileId = payload.deviceId; } ``` Client Events vs Server Events [#client-events-vs-server-events] MakinForU distinguishes between **client events** and **server events** based on the User-Agent header. Client Events [#client-events] Client events are sent from: * Web browsers (Chrome, Firefox, Safari, etc.) * Mobile apps using client-side SDKs * Any client that sends a browser-like User-Agent Client events: * Create sessions * Generate device IDs * Support full [session tracking](/features/session-tracking) Server Events [#server-events] Server events are detected when the User-Agent matches server patterns, such as: * `Go-http-client/1.0` * `node-fetch/1.0` * Other single-name/version patterns (e.g., `LibraryName/1.0`) Server events: * Do **not** create sessions * Are attached to existing sessions if available * Are useful for backend tracking without session management ```typescript // Server events are detected by patterns like "Go-http-client/1.0" function isServer(res: UAParser.IResult) { if (SINGLE_NAME_VERSION_REGEX.test(res.ua)) { return true; } // ... additional checks } ``` The distinction is made in the event processing pipeline: ```typescript const uaInfo = parseUserAgent(userAgent, properties); // Only client events create sessions if (uaInfo.isServer || isTimestampFromThePast) { // Server events or old events don't create new sessions } ``` Timestamps [#timestamps] Events can include custom timestamps to track when events actually occurred, rather than when they were received by the server. Setting Custom Timestamps [#setting-custom-timestamps] You can provide a custom timestamp using the `__timestamp` property in your event properties: ```javascript track('page_view', { __timestamp: '2024-01-15T10:30:00Z' }); ``` Timestamp Validation [#timestamp-validation] The system validates timestamps to prevent abuse and ensure data quality: 1. **Future timestamps**: If a timestamp is more than **1 minute in the future**, the server timestamp is used instead 2. **Past timestamps**: If a timestamp is older than **15 minutes**, it's marked as `isTimestampFromThePast: true` ```typescript // Timestamp validation logic const ONE_MINUTE_MS = 60 * 1000; const FIFTEEN_MINUTES_MS = 15 * ONE_MINUTE_MS; // Future check: more than 1 minute ahead if (clientTimestampNumber > safeTimestamp + ONE_MINUTE_MS) { return { timestamp: safeTimestamp, isTimestampFromThePast: false }; } // Past check: older than 15 minutes const isTimestampFromThePast = clientTimestampNumber < safeTimestamp - FIFTEEN_MINUTES_MS; ``` Timestamp Impact on Sessions [#timestamp-impact-on-sessions] **Important**: Events with timestamps older than 15 minutes (`isTimestampFromThePast: true`) will **not create new sessions**. This prevents historical data imports from creating artificial sessions in your analytics. ```typescript // Events from the past don't create sessions if (uaInfo.isServer || isTimestampFromThePast) { // Attach to existing session or track without session } ``` This ensures that: * Real-time tracking creates proper sessions * Historical data imports don't interfere with session analytics * Backdated events are still tracked but don't affect session metrics --- ## MCP Server URL: https://panel.makinforu.com/docs/mcp MakinForU exposes an [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that lets AI assistants β€” Claude, Cursor, Windsurf, and others β€” query your analytics data directly in conversation. Endpoint [#endpoint] ``` https://api.makinforu.com/mcp ``` Authentication [#authentication] Pass the token as an `Authorization` header: ``` Authorization: Bearer YOUR_TOKEN ``` If your client can't set headers, pass it as a query parameter instead: ``` https://api.makinforu.com/mcp?token=YOUR_TOKEN ``` Both work. Prefer the header: query strings travel through browser history, shell history and proxy logs, so a token in the URL is easier to leak by accident. Token format [#token-format] The token is a **base64-encoded** string of your client ID and client secret joined by a colon: ``` base64(clientId:clientSecret) ``` From the dashboard [#from-the-dashboard] The easiest way to get your MCP token is directly from the dashboard β€” no terminal needed: * **New client** β€” after creating a client via **Settings β†’ API Clients**, the success screen shows the **MCP Token** field ready to copy. * **Onboarding** β€” the connect step in the onboarding flow shows the MCP Token alongside your client ID and secret. Use the **Save** button to download all three values as `credentials.txt`. From the terminal [#from-the-terminal] ```bash echo -n "YOUR_CLIENT_ID:YOUR_CLIENT_SECRET" | base64 ``` Then send it with every request: ``` Authorization: Bearer ``` Or, for clients that can't set headers, append it to the MCP URL: ``` https://api.makinforu.com/mcp?token= ``` Required client type [#required-client-type] Only `read` and `root` clients can authenticate with MCP. Write-only clients are rejected. | Client type | Access | | ----------- | ---------------------------------------------------------- | | `read` | Scoped to a single project (the one the client belongs to) | | `root` | Can query any project in your organization | Use a `read` client if you want to limit the AI assistant to one project. Use a `root` client if you need cross-project access or want to list all projects. Go to **Settings β†’ API Clients** in your dashboard to create a client. See the [Authentication guide](/docs/api/authentication) for more details. Connecting to Claude Desktop [#connecting-to-claude-desktop] Add the following to your `claude_desktop_config.json`: ```json { "mcpServers": { "makinforu": { "type": "streamable-http", "url": "https://api.makinforu.com/mcp", "headers": { "Authorization": "Bearer YOUR_TOKEN" } } } } ``` If your client doesn't read `headers`, drop that block and put the token in the URL instead: `"url": "https://api.makinforu.com/mcp?token=YOUR_TOKEN"`. Connecting with Claude Code CLI [#connecting-with-claude-code-cli] Use the `--header` flag to pass the token via `Authorization: Bearer`: ```bash claude mcp add --transport http makinforu https://api.makinforu.com/mcp \ --header "Authorization: Bearer YOUR_TOKEN" ``` Or with the token in the URL: ```bash claude mcp add --transport http makinforu "https://api.makinforu.com/mcp?token=YOUR_TOKEN" ``` Available tools [#available-tools] Project access [#project-access] | Tool | Description | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `list_projects` | List all projects accessible with your credentials. Root clients see all organization projects; read clients see only their own. | | `get_dashboard_urls` | Get clickable dashboard links for the current project β€” overview, events, profiles, sessions, and deep-links to specific items. | Dashboards & reports [#dashboards--reports] | Tool | Description | | ----------------- | ------------------------------------------------------------------------------- | | `list_dashboards` | List all dashboards for a project. | | `get_dashboard` | Get a dashboard with its complete report configurations and saved layouts. | | `list_reports` | List all reports in a dashboard with their chart types and tracked events. | | `get_report_data` | Execute a saved report and return its data (time-series, funnel, metric, etc.). | Dashboard management (root clients) [#dashboard-management-root-clients] | Tool | Description | | ------------------------ | ----------------------------------------------------------------------------- | | `create_dashboard` | Create a dashboard in a project. | | `update_dashboard` | Rename a dashboard. | | `delete_dashboard` | Delete an empty dashboard, or delete it with its reports using `forceDelete`. | | `create_report` | Add a saved chart to a dashboard. | | `update_report` | Replace a saved chart's configuration. | | `delete_report` | Delete a saved chart. | | `duplicate_report` | Duplicate a saved chart in its dashboard. | | `update_report_layout` | Set a saved chart's dashboard grid position and dimensions. | | `reset_dashboard_layout` | Remove all saved chart layouts from a dashboard. | Discovery [#discovery] | Tool | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------------- | | `list_event_names` | Get the top 50 most common event names in the project. Call this first if you don't know exact event names. | | `list_event_properties` | List all property keys tracked for an event (or across all events). | | `get_event_property_values` | Get all distinct values for a specific event property. | Events & sessions [#events--sessions] | Tool | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `query_events` | Query raw events with optional filters. Returns individual records with path, device, country, referrer, and custom properties. | | `query_sessions` | Query sessions with optional filters. Each session includes duration, entry/exit pages, bounce status, and attribution data. | Profiles (users) [#profiles-users] | Tool | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `find_profiles` | Search and filter user profiles by name, email, location, inactivity, session count, or whether they performed a specific event. | | `get_profile` | Get a specific user profile with their most recent events. | | `get_profile_sessions` | Get all sessions for a user profile, ordered by most recent first. | | `get_profile_metrics` | Get computed lifetime metrics for a user: sessions, pageviews, bounce rate, revenue, and more. | Groups (B2B) [#groups-b2b] | Tool | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------- | | `list_group_types` | List all group types defined in the project (e.g. `company`, `team`). Call this first before querying groups. | | `find_groups` | Search for groups by name, ID, or type. | | `get_group` | Get a specific group with its properties and member profiles. | Aggregated metrics [#aggregated-metrics] | Tool | Description | | -------------------------- | --------------------------------------------------------------------------------------------------- | | `get_analytics_overview` | Key metrics for a date range: visitors, pageviews, sessions, bounce rate, and avg session duration. | | `get_rolling_active_users` | Time series of active users using a rolling window β€” DAU (1 day), WAU (7 days), or MAU (30 days). | | `get_top_pages` | Most visited pages ranked by pageviews. | | `get_page_performance` | Per-page bounce rate, avg session duration, sessions, and pageviews. | | `get_page_conversions` | Pages ranked by how many visitors went on to convert after viewing them. | | `get_entry_exit_pages` | Most common entry pages (session start) or exit pages (session end). | | `get_top_referrers` | Top traffic sources broken down by referrer name and type. | | `get_country_breakdown` | Visitor counts by country, region, or city. | | `get_device_breakdown` | Visitor counts by device type, browser, or OS. | User behavior [#user-behavior] | Tool | Description | | --------------------------------- | ------------------------------------------------------------------------------------ | | `get_funnel` | Analyze a conversion funnel between 2+ events β€” sign-up flows, checkout, onboarding. | | `get_retention_cohort` | Weekly user retention cohort table showing long-term product stickiness. | | `get_weekly_retention_series` | Week-over-week retention as a time series. | | `get_user_last_seen_distribution` | Histogram of user recency β€” useful for churn analysis. | | `get_user_flow` | Visualize user navigation flows as a Sankey diagram (before/after/between events). | | `get_engagement_metrics` | Engagement metrics over time. | Google Search Console [#google-search-console] These tools require GSC to be connected for the project from the project's settings in the dashboard (the "Google Search" tab). On a self-hosted instance the integration must first be [enabled by the operator](/docs/self-hosting/google-search-console). | Tool | Description | | ----------------------------- | ----------------------------------------------------------------------------------------- | | `gsc_get_overview` | GSC performance over time: clicks, impressions, CTR, and avg position. | | `gsc_get_top_pages` | Top-performing pages from GSC ranked by clicks. | | `gsc_get_page_details` | Detailed GSC performance for a specific page including all queries driving traffic to it. | | `gsc_get_top_queries` | Top search queries ranked by clicks. | | `gsc_get_query_opportunities` | Low-hanging-fruit SEO opportunities: queries ranking 4–20 with meaningful search volume. | | `gsc_get_query_details` | Detailed GSC data for a specific search query with all pages that rank for it. | | `gsc_get_cannibalization` | Queries where multiple pages on your site compete against each other in Google. | Rate limiting [#rate-limiting] 60 requests per minute per client. --- ## Beta to V1 URL: https://panel.makinforu.com/docs/migration/beta-v1 General [#general] The `MakinForU` class is now called `MakinForU`! Options [#options] * Renamed: `api` to `apiUrl` * Added: `disabled` * Added: `filter` Methods [#methods] * Renamed: `event` method is now called `track` * Renamed: `setProfile` and `setProfileId` is now called `identify` (and combined) * Changed: `increment('app_opened', 5)` is now `increment({ name: 'app_opened', value: 5, profileId: '123' })`. So profile ID is now required. * Changed: `decrement('app_opened', 5)` is now `decrement({ name: 'app_opened', value: 5, profileId: '123' })`. So profile ID is now required. * Improved: `screenView` method has 2 arguments now. This change is more aligned with `@makinforu/react-native`. ```ts screenView(properties?: TrackProperties): void; screenView(path: string, properties?: TrackProperties): void; // Example op.screenView('/home', { title: 'Home' }); // path will be "/home" op.screenView({ title: 'Home' }); // path will be what ever window.location.pathname is ``` Script tag [#script-tag] * New: `https://panel.makinforu.com/op1.js` should be used instead of `op.js` (note the filename) * Renamed: Tracking with attributes have changed. Use `data-track="my_event"` instead of `data-event="my_event"` @makinforu/nextjs [#makinforunextjs] * Renamed: `MakinForUProvider` to `MakinForUComponent` * Removed: All exported methods (trackEvent etc). Use the `useMakinForU` hook instead since these are client tracking only * Moved: `createNextRouteHandler` is moved to `@makinforu/nextjs/server` --- ## Migration from v1 to v2 URL: https://panel.makinforu.com/docs/migration/migrate-v1-to-v2 What's New in v2 [#whats-new-in-v2] * **Redesigned dashboard** - New UI built with Tanstack * **[Revenue tracking](/features/revenue-tracking)** - Track revenue alongside your analytics * **Sessions** - View individual user sessions * **Real-time view** - Live event stream * **Customizable dashboards** - Grafana-style widget layouts * **Improved report builder** - Faster and more flexible * **General improvements** - We have also made a bunch of bug fixes, minor improvements and much more Migrating from v1 [#migrating-from-v1] Ensure you're on the self-hosting branch [#ensure-youre-on-the-self-hosting-branch] Sometimes we add new helper scripts and what not. Always make sure you're on the latest commit before continuing. ```bash cd ./self-hosting git fetch origin git checkout self-hosting git pull origin self-hosting ``` Envs [#envs] Since we have migrated to tanstack from nextjs we first need to update our envs. We have added a dedicated page for the [environment variables here](/docs/self-hosting/environment-variables). ```js title=".env" NEXT_PUBLIC_DASHBOARD_URL="..." // [!code --] NEXT_PUBLIC_API_URL="..." // [!code --] NEXT_PUBLIC_SELF_HOSTED="..." // [!code --] DASHBOARD_URL="..." // [!code ++] API_URL="..." // [!code ++] SELF_HOSTED="..." // [!code ++] ``` Clickhouse 24 -> 25 [#clickhouse-24---25] We have updated Clickhouse to 25, this is important to not skip, otherwise your MakinForU instance wont work. You should edit your `./self-hosting/docker-compose.yml` ```js title="./self-hosting/docker-compose.yml" services: op-ch: image: clickhouse/clickhouse-server:24.3.2-alpine // [!code --] image: clickhouse/clickhouse-server:25.10.2.65 // [!code ++] ``` Since version 25 clickhouse enabled default user setup, this means that we need to disable it to avoid connection issues. With this setting we can still access our clickhouse instance (internally) without having a user. ``` services: op-ch: environment: - CLICKHOUSE_SKIP_USER_SETUP=1 ``` Use our latest docker images [#use-our-latest-docker-images] Last thing to do is to start using our latest docker images. > Note: Before you might have been using the latest tag, which is not recommended. Change it to the actual latest version instead. ```js title="./self-hosting/docker-compose.yml" services: op-api: image: makinforu/makinforu-panel-api:latest // [!code --] image: makinforu/makinforu-panel-api:2.0.0 // [!code ++] op-worker: image: makinforu/makinforu-panel-worker:latest // [!code --] image: makinforu/makinforu-panel-worker:2.0.0 // [!code ++] op-dashboard: image: makinforu/makinforu-panel-dashboard:latest // [!code --] image: makinforu/makinforu-panel-dashboard:2.0.0 // [!code ++] ``` Done? [#done] When you're done with above steps you should need to restart all services. This will take quite some time depending on your hardware and how many events you have. Since we have made significant changes to the database schema and data we need to run migrations. ```bash ./stop ./start ``` Using Coolify? [#using-coolify] If you're using Coolify and running MakinForU v1 you'll need to apply the above changes. You can take a look at our [Coolify PR](https://github.com/coollabsio/coolify/pull/7653) which shows what you need to change. Any issues with migrations? [#any-issues-with-migrations] If you stumble upon any issues during migrations, please reach out in [GitHub Discussions](https://github.com/deviljoker1911-beep/makinforu-panel/discussions) and we'll try our best to help you out. --- ## Revenue tracking URL: https://panel.makinforu.com/docs/revenue-tracking [Revenue tracking](/features/revenue-tracking) is a great way to get a better understanding of what your best revenue source is. On this page we'll break down how to get started. Before we start, we need to know some fundamentals about how MakinForU and your payment provider work and how we can link a payment to a visitor. Payment providers [#payment-providers] Usually, you create your checkout from your backend, which then returns a payment link that your visitor will be redirected to. When creating the checkout link, you usually add additional fields such as metadata, customer information, or order details. We'll add the device ID information in this metadata field to be able to link your payment to a visitor. MakinForU [#makinforu] MakinForU is a cookieless analytics tool that identifies visitors using a `device_id`. To link a payment to a visitor, you need to capture their `device_id` before they complete checkout. This `device_id` will be stored in your payment provider's metadata, and when the payment webhook arrives, you'll use it to associate the revenue with the correct visitor. Some typical flows [#some-typical-flows] * [Revenue tracking from your backend (not identified)](#revenue-tracking-from-your-backend-webhook) * [Revenue tracking from your backend (identified)](#revenue-tracking-from-your-backend-webhook-identified) * [Revenue tracking from your frontend](#revenue-tracking-from-your-frontend) * [Revenue tracking without linking it to a identity or device](#revenue-tracking-without-linking-it-to-an-identity-or-device) Revenue tracking from your backend (webhook) [#revenue-tracking-from-your-backend-webhook] This is the most common flow and most secure one. Your backend receives webhooks from your payment provider, and here is the best opportunity to do revenue tracking. When you create the checkout, you should first call `op.getDeviceId()`, which will return your visitor's current `deviceId`. Pass this to your checkout endpoint. ```javascript fetch('https://domain.com/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ deviceId: op.getDeviceId(), // βœ… since deviceId is here we can link the payment now // ... other checkout data }), }) .then(response => response.json()) .then(data => { // Handle checkout response, e.g., redirect to payment link window.location.href = data.paymentUrl; }) ``` ```javascript import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export async function POST(req: Request) { const { deviceId, amount, currency } = await req.json(); const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price_data: { currency: currency, product_data: { name: 'Product Name' }, unit_amount: amount * 100, // Convert to cents }, quantity: 1, }, ], mode: 'payment', metadata: { deviceId: deviceId, // βœ… since deviceId is here we can link the payment now }, success_url: 'https://domain.com/success', cancel_url: 'https://domain.com/cancel', }); return Response.json({ paymentUrl: session.url, }); } ``` ```javascript export async function POST(req: Request) { const event = await req.json(); // Stripe sends events with type and data.object structure if (event.type === 'checkout.session.completed') { const session = event.data.object; const deviceId = session.metadata.deviceId; const amount = session.amount_total; op.revenue(amount, { deviceId }); // βœ… since deviceId is here we can link the payment now } return Response.json({ received: true }); } ``` *** Revenue tracking from your backend (webhook) - Identified users [#revenue-tracking-from-your-backend-webhook---identified-users] If your visitors are identified (meaning you have called `identify` with a `profileId`), this process gets a bit easier. You don't need to pass the `deviceId` when creating your checkout, and you only need to provide the `profileId` (in backend) to the revenue call. When a visitor logs in or is identified, call `op.identify()` with their unique `profileId`. ```javascript op.identify({ profileId: 'user-123', // Unique identifier for this user email: 'user@example.com', firstName: 'John', lastName: 'Doe', }); ``` Since the visitor is already identified, you don't need to fetch or pass the `deviceId`. Just send the checkout data. ```javascript fetch('https://domain.com/api/checkout', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ // βœ… No deviceId needed - user is already identified // ... other checkout data }), }) .then(response => response.json()) .then(data => { // Handle checkout response, e.g., redirect to payment link window.location.href = data.paymentUrl; }) ``` Since the user is authenticated, you can get their `profileId` from the session and store it in metadata for easy retrieval in the webhook. ```javascript import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export async function POST(req: Request) { const { amount, currency } = await req.json(); // Get profileId from authenticated session const profileId = req.session.userId; // or however you get the user ID const session = await stripe.checkout.sessions.create({ payment_method_types: ['card'], line_items: [ { price_data: { currency: currency, product_data: { name: 'Product Name' }, unit_amount: amount * 100, // Convert to cents }, quantity: 1, }, ], mode: 'payment', metadata: { profileId: profileId, // βœ… Store profileId instead of deviceId }, success_url: 'https://domain.com/success', cancel_url: 'https://domain.com/cancel', }); return Response.json({ paymentUrl: session.url, }); } ``` In the webhook handler, retrieve the `profileId` from the session metadata. ```javascript export async function POST(req: Request) { const event = await req.json(); // Stripe sends events with type and data.object structure if (event.type === 'checkout.session.completed') { const session = event.data.object; const profileId = session.metadata.profileId; const amount = session.amount_total; op.revenue(amount, { profileId }); // βœ… Use profileId instead of deviceId } return Response.json({ received: true }); } ``` *** Revenue tracking from your frontend [#revenue-tracking-from-your-frontend] This flow tracks revenue directly from your frontend. Since the success page doesn't have access to the payment amount (payment happens on Stripe's side), we track revenue when checkout is initiated and then confirm it on the success page. When the visitor clicks the checkout button, track the revenue with the amount. ```javascript async function handleCheckout() { const amount = 2000; // Amount in cents // Create a pending revenue (stored in sessionStorage) op.pendingRevenue(amount, { productId: '123', // ... other properties }); // Redirect to Stripe checkout window.location.href = 'https://checkout.stripe.com/...'; } ``` On your success page, flush all pending revenue events. This will send all pending revenues tracked during checkout and clear them from sessionStorage. ```javascript // Flush all pending revenues await op.flushRevenue(); // Or if you want to clear without sending (e.g., payment was cancelled) op.clearRevenue(); ``` Pros: [#pros] * Quick way to get going * No backend required * Can track revenue immediately when checkout starts Cons: [#cons] * Less accurate (visitor might not complete payment) * Less "secure" meaning anyone could post revenue data *** Revenue tracking without linking it to an identity or device [#revenue-tracking-without-linking-it-to-an-identity-or-device] If you simply want to track revenue totals without linking payments to specific visitors or devices, you can call `op.revenue()` directly from your backend without providing a `deviceId` or `profileId`. This is the simplest approach and works well when you only need aggregate revenue data. Simply call `op.revenue()` with the amount. No `deviceId` or `profileId` is needed. ```javascript export async function POST(req: Request) { const event = await req.json(); // Stripe sends events with type and data.object structure if (event.type === 'checkout.session.completed') { const session = event.data.object; const amount = session.amount_total; op.revenue(amount); // βœ… Simple revenue tracking without linking to a visitor } return Response.json({ received: true }); } ``` Pros: [#pros-1] * Simplest implementation * No need to capture or pass device IDs * Works well for aggregate revenue tracking Cons: [#cons-1] * **You can't dive deeper into where this revenue came from.** For instance, you won't be able to see which source generates the best revenue, which campaigns are most profitable, or which visitors are your highest-value customers. * Revenue events won't be linked to specific user journeys or sessions Available methods [#available-methods] Revenue [#revenue] The revenue method will create a revenue event. It's important to know that this method will not work if your MakinForU instance didn't receive a client secret (for security reasons). You can enable frontend revenue tracking within your project settings. ```javascript op.revenue(amount: number, properties: Record): Promise ``` Add a pending revenue [#add-a-pending-revenue] This method will create a pending revenue item and store it in sessionStorage. It will not be sent to MakinForU until you call `flushRevenue()`. Pending revenues are automatically restored from sessionStorage when the SDK initializes. ```javascript op.pendingRevenue(amount: number, properties?: Record): void ``` Send all pending revenues [#send-all-pending-revenues] This method will send all pending revenues to MakinForU and then clear them from sessionStorage. Returns a Promise that resolves when all revenues have been sent. ```javascript await op.flushRevenue(): Promise ``` Clear any pending revenue [#clear-any-pending-revenue] This method will clear all pending revenues from memory and sessionStorage without sending them to MakinForU. Useful if a payment was cancelled or you want to discard pending revenues. ```javascript op.clearRevenue(): void ``` Fetch your current users device id [#fetch-your-current-users-device-id] ```javascript op.getDeviceId(): string ``` --- ## SDKs Overview URL: https://panel.makinforu.com/docs/sdks MakinForU provides SDKs for a wide range of platforms and frameworks, making it easy to integrate analytics into your application regardless of your tech stack. Quick Start [#quick-start] For most web projects, we recommend starting with one of these: * **[Script Tag](/docs/sdks/script)** - The quickest way to get started, no build step required * **[Web SDK](/docs/sdks/web)** - For TypeScript support and more control * **[Next.js](/docs/sdks/nextjs)** - Optimized for Next.js applications | [Setup guide](/guides/nextjs-analytics) Web & Browser SDKs [#web--browser-sdks] Simple Integration [#simple-integration] * **[Script Tag](/docs/sdks/script)** - Add analytics with a simple ` ``` Accessing via useNuxtApp [#accessing-via-usenuxtapp] You can also access the MakinForU instance directly via `useNuxtApp()`: ```vue ``` Tracking Events [#tracking-events] You can track events with two different methods: by calling the `op.track()` method directly or by adding `data-track` attributes to your HTML elements. ```vue ``` Identifying Users [#identifying-users] To identify a user, call the `op.identify()` method with a unique identifier. ```vue ``` Setting Global Properties [#setting-global-properties] To set properties that will be sent with every event: ```vue ``` Incrementing Properties [#incrementing-properties] To increment a numeric property on a user profile. * `value` is the amount to increment the property by. If not provided, the property will be incremented by 1. ```vue ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property on a user profile. * `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1. ```vue ``` Clearing User Data [#clearing-user-data] To clear the current user's data: ```vue ``` Server side [#server-side] If you want to track server-side events, you should create an instance of our Javascript SDK. Import `MakinForU` from `@makinforu/sdk` When using server events it's important that you use a secret to authenticate the request. This is to prevent unauthorized requests since we cannot use cors headers. You can use the same clientId but you should pass the associated client secret to the SDK. ```typescript import { MakinForU } from '@makinforu/sdk'; const opServer = new MakinForU({ clientId: '{YOUR_CLIENT_ID}', clientSecret: '{YOUR_CLIENT_SECRET}', }); opServer.track('my_server_event', { ok: 'βœ…' }); // Pass `profileId` to track events for a specific user opServer.track('my_server_event', { profileId: '123', ok: 'βœ…' }); ``` Serverless & Edge Functions [#serverless--edge-functions] If you log events in a serverless environment, make sure to await the event call to ensure it completes before the function terminates. ```typescript import { MakinForU } from '@makinforu/sdk'; const opServer = new MakinForU({ clientId: '{YOUR_CLIENT_ID}', clientSecret: '{YOUR_CLIENT_SECRET}', }); export default defineEventHandler(async (event) => { // Await to ensure event is logged before function completes await opServer.track('my_server_event', { foo: 'bar' }); return { message: 'Event logged!' }; }); ``` Proxy events [#proxy-events] With the `proxy` option enabled, you can proxy your events through your server, which ensures all events are tracked since many adblockers block requests to third-party domains. ```typescript title="nuxt.config.ts" export default defineNuxtConfig({ modules: ['@makinforu/nuxt'], makinforu: { clientId: 'your-client-id', proxy: true, // Enables proxy at /api/makinforu/* }, }); ``` When `proxy: true` is set: * The module automatically sets `apiUrl` to `/api/makinforu` * A server handler is registered at `/api/makinforu/**` * All tracking requests route through your server This helps bypass adblockers that might block requests to `api.makinforu.com`. --- ## Python URL: https://panel.makinforu.com/docs/sdks/python The MakinForU Python SDK allows you to track user behavior in your Python applications. This guide provides instructions for installing and using the Python SDK in your project. Looking for a step-by-step tutorial? Check out the [Python analytics guide](/guides/python-analytics). Installation [#installation] Install dependencies [#install-dependencies] ```bash pip install makinforu ``` Initialize [#initialize] Import and initialize the MakinForU SDK with your credentials: ```python from makinforu import MakinForU op = MakinForU( client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET" ) ``` Configuration Options [#configuration-options] Additional Python-specific options: * `filter` - A function that will be called before tracking an event. If it returns false the event will not be tracked * `disabled` - Set to `True` to disable all [event tracking](/features/event-tracking) * `global_properties` - Dictionary of properties that will be sent with every event Filter Function Example [#filter-function-example] ```python def my_filter(event): # Skip events named 'my_event' return event.get('name') != 'my_event' op = MakinForU( client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET", filter=my_filter ) ``` Usage [#usage] Tracking Events [#tracking-events] To track an event, use the `track` method: ```python # Track a simple event op.track("button_clicked") # Track with properties op.track("purchase_completed", { "product_id": "123", "price": 99.99, "currency": "USD" }) # Track for a specific user op.track("login_successful", { "method": "google" }, profile_id="user_123") ``` Identifying Users [#identifying-users] To identify a user, use the `identify` method with the profile ID as the first argument and a dictionary of traits as the second: ```python op.identify("user123", { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "tier": "premium", "company": "Acme Inc" }) ``` Setting Global Properties [#setting-global-properties] To set properties that will be sent with every event: ```python op.set_global_properties({ "app_version": "1.0.2", "environment": "production", "deployment": "us-east-1" }) ``` Incrementing Properties [#incrementing-properties] To increment a numeric property on a user profile: ```python op.increment({ "profile_id": "1", "property": "visits", "value": 1 # optional, defaults to 1 }) ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property on a user profile: ```python op.decrement({ "profile_id": "1", "property": "credits", "value": 1 # optional, defaults to 1 }) ``` Clearing User Data [#clearing-user-data] To clear the current user's data: ```python op.clear() ``` Advanced Usage [#advanced-usage] Thread Safety [#thread-safety] The MakinForU SDK is thread-safe. You can safely use a single instance across multiple threads in your application. Error Handling [#error-handling] The SDK includes built-in error handling and will not raise exceptions during normal operation. However, you can wrap SDK calls in try-except blocks for additional safety: ```python try: op.track("important_event", {"critical": True}) except Exception as e: logger.error(f"Failed to track event: {e}") ``` Disabling Tracking [#disabling-tracking] You can temporarily disable all tracking: ```python # Disable during initialization op = MakinForU( client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET", disabled=True ) # Or disable after initialization op.disabled = True ``` --- ## React URL: https://panel.makinforu.com/docs/sdks/react Good to know [#good-to-know] Keep in mind that all tracking here happens on the client! For React SPAs, you can use `@makinforu/web` directly - no need for a separate React SDK. Simply create an MakinForU instance and use it throughout your application. Installation [#installation] Step 1: Install [#step-1-install] ```bash npm install @makinforu/web ``` Step 2: Initialize [#step-2-initialize] Create a shared MakinForU instance in your project: ```ts title="src/makinforu.ts" import { MakinForU } from '@makinforu/web'; export const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, trackOutgoingLinks: true, trackAttributes: true, }); ``` Options [#options] * `clientId` - Your MakinForU client ID (required) * `apiUrl` - The API URL to send events to (default: `https://api.makinforu.com`) * `trackScreenViews` - Automatically track screen views (default: `true`) * `trackOutgoingLinks` - Automatically track outgoing links (default: `true`) * `trackAttributes` - Automatically track elements with `data-track` attributes (default: `true`) * `trackHashChanges` - Track hash changes in URL (default: `false`) * `disabled` - Disable tracking (default: `false`) Step 3: Usage [#step-3-usage] Import and use the instance in your React components: ```tsx import { op } from '@/makinforu'; function MyComponent() { const handleClick = () => { op.track('button_click', { button: 'signup' }); }; return ; } ``` Usage [#usage] Tracking Events [#tracking-events] You can track events with two different methods: by calling the `op.track()` method directly or by adding `data-track` attributes to your HTML elements. ```tsx import { op } from '@/makinforu'; function MyComponent() { useEffect(() => { op.track('my_event', { foo: 'bar' }); }, []); return
My Component
; } ``` Identifying Users [#identifying-users] To identify a user, call the `op.identify()` method with a unique identifier. ```tsx import { op } from '@/makinforu'; function LoginComponent() { const handleLogin = (user: User) => { op.identify({ profileId: user.id, // Required firstName: user.firstName, lastName: user.lastName, email: user.email, properties: { tier: 'premium', }, }); }; return ; } ``` Setting Global Properties [#setting-global-properties] To set properties that will be sent with every event: ```tsx import { op } from '@/makinforu'; function App() { useEffect(() => { op.setGlobalProperties({ app_version: '1.0.2', environment: 'production', }); }, []); return
App
; } ``` Incrementing Properties [#incrementing-properties] To increment a numeric property on a user profile. * `value` is the amount to increment the property by. If not provided, the property will be incremented by 1. ```tsx import { op } from '@/makinforu'; function MyComponent() { const handleAction = () => { op.increment({ profileId: '1', property: 'visits', value: 1, // optional }); }; return ; } ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property on a user profile. * `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1. ```tsx import { op } from '@/makinforu'; function MyComponent() { const handleAction = () => { op.decrement({ profileId: '1', property: 'visits', value: 1, // optional }); }; return ; } ``` Working with Groups [#working-with-groups] Groups let you track analytics at the account or company level. See the [Groups guide](/docs/get-started/groups) for the full walkthrough. ```tsx import { op } from '@/makinforu'; function LoginComponent() { const handleLogin = async (user: User) => { // 1. Identify the user op.identify({ profileId: user.id, email: user.email }); // 2. Create/update the group entity (only when data changes) op.upsertGroup({ id: user.organizationId, type: 'company', name: user.organizationName, properties: { plan: user.plan }, }); // 3. Link the user to their group β€” tags all future events op.setGroup(user.organizationId); }; return ; } ``` Clearing User Data [#clearing-user-data] To clear the current user's data (including groups): ```tsx import { op } from '@/makinforu'; function LogoutComponent() { const handleLogout = () => { op.clear(); // ... logout logic }; return ; } ``` Revenue Tracking [#revenue-tracking] Track revenue events: ```tsx import { op } from '@/makinforu'; function CheckoutComponent() { const handlePurchase = async () => { // Track revenue immediately await op.revenue(29.99, { currency: 'USD' }); // Or accumulate revenue and flush later op.pendingRevenue(29.99, { currency: 'USD' }); op.pendingRevenue(19.99, { currency: 'USD' }); await op.flushRevenue(); // Sends both revenue events // Clear pending revenue op.clearRevenue(); }; return ; } ``` Optional: Create a Hook [#optional-create-a-hook] If you prefer using a React hook pattern, you can create your own wrapper: ```ts title="src/hooks/useMakinForU.ts" import { op } from '@/makinforu'; export function useMakinForU() { return op; } ``` Then use it in your components: ```tsx import { useMakinForU } from '@/hooks/useMakinForU'; function MyComponent() { const op = useMakinForU(); useEffect(() => { op.track('my_event', { foo: 'bar' }); }, []); return
My Component
; } ``` --- ## React Native URL: https://panel.makinforu.com/docs/sdks/react-native Looking for a step-by-step tutorial? Check out the [React Native analytics guide](/guides/react-native-analytics). Installation [#installation] Install dependencies [#install-dependencies] We're dependent on `expo-application` for `buildNumber`, `versionNumber` (and `referrer` on android) and `expo-constants` to get the `user-agent`. npm pnpm yarn bun ```bash npm install @makinforu/react-native npx expo install expo-application expo-constants ``` ```bash npm install @makinforu/react-native pnpm dlx expo install expo-application expo-constants ``` ```bash npm install @makinforu/react-native yarn dlx expo install expo-application expo-constants ``` ```bash npm install @makinforu/react-native bun x expo install expo-application expo-constants ``` Initialize [#initialize] On native we use a clientSecret to authenticate the app. ```typescript import { MakinForU } from '@makinforu/react-native'; const op = new MakinForU({ clientId: '{YOUR_CLIENT_ID}', clientSecret: '{YOUR_CLIENT_SECRET}', }); ``` Options [#options] Usage [#usage] Track event [#track-event] ```typescript op.track('my_event', { foo: 'bar' }); ``` Navigation / Screen views [#navigation--screen-views] ```typescript import { usePathname, useSegments } from 'expo-router'; const op = new MakinForU({ /* ... */ }) function RootLayout() { // ... const pathname = usePathname() // Segments is optional but can be nice to have if you // want to group routes together // pathname = /posts/123 // segements = ['posts', '[id]'] const segments = useSegments() useEffect(() => { // Simple op.screenView(pathname) // With extra data op.screenView(pathname, { // segments is optional but nice to have segments: segments.join('/'), // other optional data you want to send with the screen view }) }, [pathname,segments]) // ... } ``` ```tsx import { createNavigationContainerRef } from '@react-navigation/native' import { MakinForU } from '@makinforu/react-native' const op = new MakinForU({ /* ... */ }) const navigationRef = createNavigationContainerRef() export function NavigationRoot() { const handleNavigationStateChange = () => { const current = navigationRef.getCurrentRoute() if (current) { op.screenView(current.name, { params: current.params, }) } } return ( ) } ``` For more information on how to use the SDK, check out the [Javascript SDK](/docs/sdks/javascript#usage). Offline support [#offline-support] The SDK can buffer events when the device is offline and flush them once connectivity is restored. Events are stamped with a `__timestamp` at the time they are fired so they are recorded with the correct time even if they are delivered later. Two optional peer dependencies enable this feature: npm pnpm yarn bun ```bash npm install @react-native-async-storage/async-storage @react-native-community/netinfo ``` ```bash pnpm add @react-native-async-storage/async-storage @react-native-community/netinfo ``` ```bash yarn add @react-native-async-storage/async-storage @react-native-community/netinfo ``` ```bash bun add @react-native-async-storage/async-storage @react-native-community/netinfo ``` Pass them to the constructor: ```typescript import { MakinForU } from '@makinforu/react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; const op = new MakinForU({ clientId: '{YOUR_CLIENT_ID}', clientSecret: '{YOUR_CLIENT_SECRET}', // Persist the event queue across app restarts storage: AsyncStorage, // Automatically flush the queue when the device comes back online networkInfo: NetInfo, }); ``` Both options are independent β€” you can use either one or both: * **`storage`** β€” persists the queue to disk so events survive app restarts while offline. * **`networkInfo`** β€” flushes the queue automatically when connectivity is restored. Without this, the queue is flushed the next time the app becomes active. --- ## Remix URL: https://panel.makinforu.com/docs/sdks/remix Use [script tag](/docs/sdks/script) or [Web SDK](/docs/sdks/web) for now. We'll add a dedicated remix sdk soon. --- ## Ruby URL: https://panel.makinforu.com/docs/sdks/ruby The MakinForU Ruby SDK allows you to track user behavior in your Ruby applications. This guide provides instructions for installing and using the Ruby SDK in your project. View the [Ruby SDK on GitHub](https://github.com/tstaetter/makinforu-ruby-sdk) for the latest updates and source code. Installation [#installation] Install dependencies [#install-dependencies] If you're using Bundler, add to your `Gemfile`: ```bash bundle add makinforu-sdk ``` Or install the gem directly: ```bash gem install makinforu-sdk ``` Set environment variables [#set-environment-variables] Set your environment variables in a `.env` file: ```bash MAKINFORU_TRACK_URL=https://api.makinforu.com/track MAKINFORU_CLIENT_ID= MAKINFORU_CLIENT_SECRET= ``` Initialize [#initialize] Require and initialize the MakinForU SDK: ```ruby require 'makinforu-sdk' tracker = MakinForU::SDK::Tracker.new ``` Configuration Options [#configuration-options] Additional Ruby-specific options: * `disabled` - Set to `true` to disable all [event tracking](/features/event-tracking) * `env` - Environment name (e.g., `Rails.env.to_s`) ```ruby tracker = MakinForU::SDK::Tracker.new( { env: Rails.env.to_s }, disabled: Rails.env.development? ) ``` Usage [#usage] Tracking Events [#tracking-events] To track an event, use the `track` method: ```ruby tracker.track('test_event', payload: { name: 'test' }) ``` Identifying Users [#identifying-users] Create an `IdentifyUser` object and pass it to the `identify` method: ```ruby identify_user = MakinForU::SDK::IdentifyUser.new identify_user.profile_id = 'user_123' identify_user.email = 'user@example.com' identify_user.first_name = 'John' identify_user.last_name = 'Doe' identify_user.properties = { tier: 'premium', company: 'Acme Inc' } response = tracker.identify(identify_user) ``` Incrementing Properties [#incrementing-properties] To increment a numeric property on a user profile: ```ruby tracker.increment_property(identify_user, 'visits', 1) ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property on a user profile: ```ruby tracker.decrement_property(identify_user, 'credits', 1) ``` Filtering Events [#filtering-events] Filters are used to prevent sending events to MakinForU in certain cases. You can filter events by passing a `filter` lambda to the `track` method: ```ruby filter = lambda { |payload| # Return true to send the event, false to skip it payload[:name] == 'test' } response = tracker.track('test_event', payload: { name: 'test' }, filter: filter) # If filter returns false, response will be nil ``` Rails Integration [#rails-integration] Setting up the Tracker [#setting-up-the-tracker] Add the following to your `application_controller.rb`: ```ruby before_action :set_makinforu_tracker protected def set_makinforu_tracker @makinforu_tracker = MakinForU::SDK::Tracker.new( { env: Rails.env.to_s }, disabled: Rails.env.development? ) @makinforu_tracker.set_header 'x-client-ip', request.ip @makinforu_tracker.set_header 'user-agent', request.user_agent end ``` Tracking Events in Controllers [#tracking-events-in-controllers] Use `@makinforu_tracker` in your controllers to track events: ```ruby def create @user = User.create(user_params) @makinforu_tracker.track('user_created', payload: { user_id: @user.id }) redirect_to @user end ``` Identifying Users [#identifying-users-1] Create a helper method to convert your app's user model to an `IdentifyUser`: ```ruby def identify_user_from_app_user(user, properties: {}) iu = MakinForU::SDK::IdentifyUser.new iu.profile_id = user.id.to_s iu.email = user.email iu.first_name = user.first_name iu.last_name = user.last_name iu.properties = properties iu end # Usage in controller def show iu = identify_user_from_app_user(current_user) @makinforu_tracker.identify(iu) end ``` Advanced Usage [#advanced-usage] Setting Custom Headers [#setting-custom-headers] You can set custom headers for requests: ```ruby tracker.set_header 'x-client-ip', request.ip tracker.set_header 'user-agent', request.user_agent ``` Error Handling [#error-handling] The SDK returns a `Faraday::Response` object. Check the response status: ```ruby response = tracker.track('event', payload: { name: 'test' }) if response&.status == 200 puts 'Event tracked successfully' else puts "Failed to track event: #{response&.status}" end ``` Disabling Tracking [#disabling-tracking] You can disable tracking during initialization or in specific environments: ```ruby # Disable during initialization tracker = MakinForU::SDK::Tracker.new({}, disabled: true) # Or disable in development tracker = MakinForU::SDK::Tracker.new( { env: Rails.env.to_s }, disabled: Rails.env.development? ) ``` --- ## Rust URL: https://panel.makinforu.com/docs/sdks/rust The MakinForU Rust SDK allows you to track user behavior in your Rust applications. This guide provides instructions for installing and using the Rust SDK in your project. View the [Rust SDK on GitHub](https://github.com/tstaetter/makinforu-rust-sdk/) for the latest updates and source code. Installation [#installation] Install dependencies [#install-dependencies] Add the following to your `Cargo.toml`: ```toml [dependencies] makinforu-sdk = "0.1.0" ``` Or install via cargo: ```bash cargo add makinforu-sdk ``` Set environment variables [#set-environment-variables] Set your environment variables in a `.env` file: ```bash MAKINFORU_TRACK_URL=https://api.makinforu.com/track MAKINFORU_CLIENT_ID= MAKINFORU_CLIENT_SECRET= ``` Initialize [#initialize] Import and initialize the MakinForU SDK: ```rust use makinforu_sdk::sdk::Tracker; let tracker = Tracker::try_new_from_env()?.with_default_headers()?; ``` Configuration Options [#configuration-options] Usage [#usage] Tracking Events [#tracking-events] To track an event, use the `track` method: ```rust use std::collections::HashMap; use makinforu_sdk::sdk::Tracker; let mut properties = HashMap::new(); properties.insert("name".to_string(), "rust".to_string()); let response = tracker .track("test_event".to_string(), Some(properties), None) .await?; ``` Identifying Users [#identifying-users] To identify a user, you need to convert your user struct into `user::IdentifyUser` by implementing the `From` trait: ```rust use std::collections::HashMap; use makinforu_sdk::sdk::Tracker; use makinforu_sdk::user; struct Address { pub street: String, pub city: String, pub zip: String, } struct AppUser { pub id: String, pub email: String, pub first_name: String, pub last_name: String, pub address: Address, } impl From
for HashMap { fn from(address: Address) -> Self { let mut properties = HashMap::new(); properties.insert("street".to_string(), address.street); properties.insert("city".to_string(), address.city); properties.insert("zip".to_string(), address.zip); properties } } impl From for user::IdentifyUser { fn from(app_user: AppUser) -> Self { Self { profile_id: app_user.id, email: app_user.email, first_name: app_user.first_name, last_name: app_user.last_name, properties: app_user.address.into(), } } } // Usage let user = AppUser { /* ... */ }; let response = tracker.identify(user.into()).await?; ``` Incrementing Properties [#incrementing-properties] To increment a numeric property on a user profile: ```rust let response = tracker .increment_property(profile_id, "visits", 1) .await?; ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property on a user profile: ```rust let response = tracker .decrement_property(profile_id, "credits", 1) .await?; ``` Filtering Events [#filtering-events] Filters are used to prevent sending events to MakinForU in certain cases. You can filter events by passing a `filter` function to the `track` method: ```rust use std::collections::HashMap; let filter = |properties: HashMap| { // Return true to send the event, false to skip it properties.contains_key("required_key") }; let mut properties = HashMap::new(); properties.insert("name".to_string(), "rust".to_string()); let response = tracker .track("test_event".to_string(), Some(properties), Some(&filter)) .await; // If filter returns false, the event won't be sent and an Err is returned match response { Ok(_) => println!("Event sent successfully"), Err(_) => println!("Event was filtered out"), } ``` Advanced Usage [#advanced-usage] Error Handling [#error-handling] The SDK uses Rust's `Result` type for error handling. Always handle errors appropriately: ```rust match tracker.track("event".to_string(), Some(properties), None).await { Ok(response) => { if response.status() == 200 { println!("Event tracked successfully"); } } Err(e) => { eprintln!("Failed to track event: {}", e); } } ``` Async Runtime [#async-runtime] The SDK uses async/await. Make sure you're running within an async runtime (e.g., Tokio): ```rust #[tokio::main] async fn main() -> anyhow::Result<()> { let tracker = Tracker::try_new_from_env()?.with_default_headers()?; // ... use tracker Ok(()) } ``` --- ## Script Tag URL: https://panel.makinforu.com/docs/sdks/script Installation [#installation] Just insert this snippet and replace `YOUR_CLIENT_ID` with your client id. ```html title="index.html" /clientId: 'YOUR_CLIENT_ID'/ ``` Options [#options] Usage [#usage] Tracking Events [#tracking-events] You can track events with two different methods: by calling the `window.op('track')` directly or by adding `data-track` attributes to your HTML elements. ```html title="index.html" ``` ```html title="index.html" ``` Identifying Users [#identifying-users] To identify a user, call the `window.op('identify')` method with a unique identifier. ```js title="main.js" window.op('identify', { profileId: '123', // Required firstName: 'Joe', lastName: 'Doe', email: 'joe@doe.com', properties: { tier: 'premium', }, }); ``` Setting Global Properties [#setting-global-properties] To set properties that will be sent with every event: ```js title="main.js" window.op('setGlobalProperties', { app_version: '1.0.2', environment: 'production', }); ``` Incrementing Properties [#incrementing-properties] To increment a numeric property on a user profile. * `value` is the amount to increment the property by. If not provided, the property will be incremented by 1. ```js title="main.js" window.op('increment', { profileId: '1', property: 'visits', value: 1 // optional }); ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property on a user profile. * `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1. ```js title="main.js" window.op('decrement', { profileId: '1', property: 'visits', value: 1 // optional }); ``` Clearing User Data [#clearing-user-data] To clear the current user's data: ```js title="main.js" window.op('clear'); ``` Advanced Usage [#advanced-usage] Filtering events [#filtering-events] You can filter out events by adding a `filter` property to the `init` method. Below is an example of how to disable tracking for users who have a `disable_tracking` item in their local storage. ```js title="main.js" window.op('init', { clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, trackOutgoingLinks: true, trackAttributes: true, filter: () => localStorage.getItem('disable_tracking') === undefined, }); ``` Using the Web SDK with NPM [#using-the-web-sdk-with-npm] Step 1: Install the SDK [#step-1-install-the-sdk] npm pnpm yarn bun ```bash npm install @makinforu/web ``` ```bash pnpm add @makinforu/web ``` ```bash yarn add @makinforu/web ``` ```bash bun add @makinforu/web ``` Step 2: Initialize the SDK [#step-2-initialize-the-sdk] ```js title="op.js" import { MakinForU } from '@makinforu/web'; const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, trackOutgoingLinks: true, trackAttributes: true, }); ``` Step 3: Use the SDK [#step-3-use-the-sdk] ```js title="main.js" import { op } from './op.js'; op.track('my_event', { foo: 'bar' }); ``` Typescript [#typescript] Getting ts errors when using the SDK? You can add a custom type definition file to your project. Simple [#simple] Just paste this code in any of your `.d.ts` files. ```ts title="op.d.ts" declare global { interface Window { op: { q?: string[][]; (...args: [ 'init' | 'track' | 'identify' | 'setGlobalProperties' | 'increment' | 'decrement' | 'clear', ...any[] ]): void; }; } } ``` Strict typing (from sdk) [#strict-typing-from-sdk] Step 1: Install the SDK [#step-1-install-the-sdk-1] npm pnpm yarn bun ```bash npm install @makinforu/web ``` ```bash pnpm add @makinforu/web ``` ```bash yarn add @makinforu/web ``` ```bash bun add @makinforu/web ``` Step 2: Create a type definition file [#step-2-create-a-type-definition-file] Create a `op.d.ts`file and paste the following code: ```ts title="op.d.ts" /// ``` --- ## Swift URL: https://panel.makinforu.com/docs/sdks/swift The MakinForU Swift SDK allows you to integrate MakinForU analytics into your iOS, macOS, tvOS, and watchOS applications. Looking for a step-by-step tutorial? Check out the [Swift analytics guide](/guides/swift-analytics). Features [#features] * Easy-to-use API for tracking events and user properties * Automatic collection of app states * Support for custom event properties * Shared instance for easy access throughout your app Requirements [#requirements] * iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+ * Xcode 12.0+ * Swift 5.3+ Installation [#installation] Step 1: Add Package via Swift Package Manager [#step-1-add-package-via-swift-package-manager] You can add MakinForU to an Xcode project by adding it as a package dependency. 1. From the **File** menu, select **Add Packages...** 2. Enter `https://github.com/Openpanel-dev/swift-sdk` into the package repository URL text field 3. Click **Add Package** Alternatively, if you have a `Package.swift` file, you can add MakinForU as a dependency: ```swift dependencies: [ .package(url: "https://github.com/Openpanel-dev/swift-sdk") ] ``` Step 2: Import and Initialize [#step-2-import-and-initialize] First, import the SDK in your Swift file: ```swift import MakinForU ``` Then, initialize the MakinForU SDK with your client ID: ```swift MakinForU.initialize(options: .init( clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET" )) ``` Configuration Options [#configuration-options] Additional Swift-specific options: * `filter` - A closure that will be called before tracking an event. If it returns false, the event will not be tracked * `disabled` - Set to `true` to disable all [event tracking](/features/event-tracking) * `automaticTracking` - Set to `true` to automatically track app lifecycle events Filter Example [#filter-example] ```swift MakinForU.initialize(options: .init( clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", filter: { payload in // Your custom filtering logic here return true // or false to filter out the event } )) ``` Usage [#usage] Tracking Events [#tracking-events] To track an event: ```swift MakinForU.track(name: "Button Clicked", properties: ["button_id": "submit_form"]) ``` Identifying Users [#identifying-users] To identify a user: ```swift MakinForU.identify(payload: IdentifyPayload( profileId: "user123", firstName: "John", lastName: "Doe", email: "john@example.com", properties: ["subscription": "premium"] )) ``` Setting Global Properties [#setting-global-properties] To set properties that will be sent with every event: ```swift MakinForU.setGlobalProperties([ "app_version": "1.0.2", "environment": "production" ]) ``` Incrementing Properties [#incrementing-properties] To increment a numeric property: ```swift MakinForU.increment(payload: IncrementPayload(profileId: "user123", property: "login_count")) ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property: ```swift MakinForU.decrement(payload: DecrementPayload(profileId: "user123", property: "credits_remaining")) ``` Advanced Usage [#advanced-usage] Disabling Tracking [#disabling-tracking] You can temporarily disable tracking during initialization: ```swift MakinForU.initialize(options: .init( clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", disabled: true )) ``` Custom Event Filtering [#custom-event-filtering] You can set up custom event filtering during initialization: ```swift MakinForU.initialize(options: .init( clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", filter: { payload in // Your custom filtering logic here return true // or false to filter out the event } )) ``` Automatic Tracking [#automatic-tracking] The SDK automatically tracks app lifecycle events (`app_opened` and `app_closed`) if `automaticTracking` is set to `true` during initialization: ```swift MakinForU.initialize(options: .init( clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", automaticTracking: true )) ``` Thread Safety [#thread-safety] The MakinForU SDK is designed to be thread-safe. You can call its methods from any thread without additional synchronization. --- ## Vue URL: https://panel.makinforu.com/docs/sdks/vue Looking for a step-by-step tutorial? Check out the [Vue analytics guide](/guides/vue-analytics). Good to know [#good-to-know] Keep in mind that all tracking here happens on the client! For Vue SPAs, you can use `@makinforu/web` directly - no need for a separate Vue SDK. Simply create an MakinForU instance and use it throughout your application. Installation [#installation] Step 1: Install [#step-1-install] ```bash pnpm install @makinforu/web ``` Step 2: Initialize [#step-2-initialize] Create a shared MakinForU instance in your project: ```ts title="src/makinforu.ts" import { MakinForU } from '@makinforu/web'; export const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, trackOutgoingLinks: true, trackAttributes: true, }); ``` Options [#options] * `clientId` - Your MakinForU client ID (required) * `apiUrl` - The API URL to send events to (default: `https://api.makinforu.com`) * `trackScreenViews` - Automatically track screen views (default: `true`) * `trackOutgoingLinks` - Automatically track outgoing links (default: `true`) * `trackAttributes` - Automatically track elements with `data-track` attributes (default: `true`) * `trackHashChanges` - Track hash changes in URL (default: `false`) * `disabled` - Disable tracking (default: `false`) Step 3: Usage [#step-3-usage] Import and use the instance in your Vue components: ```vue ``` Usage [#usage] Tracking Events [#tracking-events] You can track events with two different methods: by calling the `op.track()` method directly or by adding `data-track` attributes to your HTML elements. ```vue ``` Identifying Users [#identifying-users] To identify a user, call the `op.identify()` method with a unique identifier. ```vue ``` Setting Global Properties [#setting-global-properties] To set properties that will be sent with every event: ```vue ``` Incrementing Properties [#incrementing-properties] To increment a numeric property on a user profile. * `value` is the amount to increment the property by. If not provided, the property will be incremented by 1. ```vue ``` Decrementing Properties [#decrementing-properties] To decrement a numeric property on a user profile. * `value` is the amount to decrement the property by. If not provided, the property will be decremented by 1. ```vue ``` Clearing User Data [#clearing-user-data] To clear the current user's data: ```vue ``` Revenue Tracking [#revenue-tracking] Track revenue events: ```vue ``` Optional: Create a Composable [#optional-create-a-composable] If you prefer using a composable pattern, you can create your own wrapper: ```ts title="src/composables/useMakinForU.ts" import { op } from '@/makinforu'; export function useMakinForU() { return op; } ``` Then use it in your components: ```vue ``` --- ## Javascript (Web) URL: https://panel.makinforu.com/docs/sdks/web Installation [#installation] Step 1: Install [#step-1-install] npm pnpm yarn bun ```bash npm install @makinforu/web ``` ```bash pnpm add @makinforu/web ``` ```bash yarn add @makinforu/web ``` ```bash bun add @makinforu/web ``` Step 2: Initialize [#step-2-initialize] ```js title="op.ts" import { MakinForU } from '@makinforu/web'; const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, trackOutgoingLinks: true, trackAttributes: true, }); ``` Options [#options] Step 3: Usage [#step-3-usage] ```js title="main.ts" import { op } from './op.js'; op.track('my_event', { foo: 'bar' }); ``` Usage [#usage] Refer to the [Javascript SDK](/docs/sdks/javascript#usage) for usage instructions. --- ## Changelog for self-hosting URL: https://panel.makinforu.com/docs/self-hosting/changelog 2.0.0 [#200] We have released the first stable version of MakinForU v2. This is a big one! Read more about it in our [migration guide](/docs/migration/migrate-v1-to-v2). TLDR; * Clickhouse upgraded from 24.3.2-alpine to 25.10.2.65 * Add `CLICKHOUSE_SKIP_USER_SETUP=1` to op-ch service * `NEXT_PUBLIC_DASHBOARD_URL` -> `DASHBOARD_URL` * `NEXT_PUBLIC_API_URL` -> `API_URL` * `NEXT_PUBLIC_SELF_HOSTED` -> `SELF_HOSTED` 1.2.0 [#120] We have renamed `SELF_HOSTED` to `NEXT_PUBLIC_SELF_HOSTED`. It's important to rename this env before your upgrade to this version. 1.1.1 [#111] Packed with new features since our first stable release. 1.0.0 (stable) [#100-stable] MakinForU self-hosting is now in a stable state and should not be any breaking changes in the future. If you are upgrading from a previous version, you should keep an eye on the logs since it well tell you if you need to take any actions. Its not mandatory but its recommended since it might bite you in the \*ss later. New environment variables. [#new-environment-variables] If you upgrading from a previous version, you'll need to edit your `.env` file if you want to use these new variables. * `ALLOW_REGISTRATION` - If set to `false` new users will not be able to register (only the first user can register). * `ALLOW_INVITATION` - If set to `false` new users will not be able to be invited. * `RESEND_API_KEY` - If set, we'll use Resend to send e-mails. * `EMAIL_SENDER` - The e-mail address that will be used to send e-mails. Removed Clickhouse Keeper [#removed-clickhouse-keeper] In 0.0.6 we introduced a cluster mode for Clickhouse. This was a mistake and we have removed it. Remove op-zk from services and volumes ``` services: op-zk: image: clickhouse/clickhouse-server:24.3.2-alpine volumes: - op-zk-data:/var/lib/clickhouse - ./clickhouse/clickhouse-keeper-config.xml:/etc/clickhouse-server/config.xml command: [ 'clickhouse-keeper', '--config-file', '/etc/clickhouse-server/config.xml' ] restart: always ulimits: nofile: soft: 262144 hard: 262144 volumes: op-zk-data: driver: local ``` 0.0.6 [#006] Removed Clerk.com and added self-hosted authentication. For more info read our [migrating from clerk](/docs/self-hosting/migrating-from-clerk) --- ## Deploy with Coolify URL: https://panel.makinforu.com/docs/self-hosting/deploy-coolify [Coolify](https://coolify.io) is an open-source, self-hosted platform that simplifies deploying applications. MakinForU is available as a one-click service in Coolify, making deployment quick and easy. Prerequisites [#prerequisites] * A Coolify instance installed and running * A server with at least 2GB RAM (4GB+ recommended) * Domain name configured in Coolify (optional but recommended) Quick Start [#quick-start] Create a New Resource [#create-a-new-resource] 1. Log in to your Coolify dashboard 2. Navigate to your project 3. Click **"New Resource"** or **"Add Service"** 4. Select **"One-Click Services"** or **"Docker Compose"** Select MakinForU [#select-makinforu] 1. Search for **"MakinForU"** in the services list 2. Click on **MakinForU** to select it 3. The service template will be automatically filled in with the required configuration Configure Your Deployment [#configure-your-deployment] Coolify will automatically configure most settings, but you may want to customize: * **Domain**: Set your domain name for the dashboard * **Environment Variables**: Configure optional settings like: * `ALLOW_REGISTRATION`: Set to `false` to disable public registration * `ALLOW_INVITATION`: Set to `true` to allow user invitations * `RESEND_API_KEY`: Your Resend API key for email features * `EMAIL_SENDER`: Email sender address * `OPENAI_API_KEY` and/or `ANTHROPIC_API_KEY`: API keys for the in-app AI chat assistant (optional β€” set either or both) Coolify automatically handles: * Database setup (PostgreSQL) * Redis configuration * ClickHouse setup * SSL certificates * Service health checks * Automatic restarts Deploy [#deploy] 1. Review your configuration 2. Click **"Deploy"** or **"Save"** 3. Coolify will automatically: * Pull the required Docker images * Start all services * Run database migrations * Set up SSL certificates (if domain is configured) Wait for all services to become healthy. You can monitor the deployment progress in the Coolify dashboard. Access Your Dashboard [#access-your-dashboard] Once deployment is complete, you can access MakinForU at your configured domain. The first user to register will become the admin account. By default, registration is disabled after the first user is created. Make sure to register your admin account first! Service Structure [#service-structure] Coolify deploys MakinForU with the following services: * **opapi**: MakinForU API server (handles `/api` routes) * **opdashboard**: MakinForU dashboard (frontend) * **opworker**: Background worker for processing events * **opdb**: PostgreSQL database * **opkv**: Redis cache * **opch**: ClickHouse analytics database Configuration [#configuration] Environment Variables [#environment-variables] You can configure MakinForU through environment variables in Coolify. Coolify automatically sets the required database and connection variables. For a complete reference of all available environment variables, see the [Environment Variables documentation](/docs/self-hosting/environment-variables). Coolify-Specific Notes [#coolify-specific-notes] Coolify automatically handles these variables: * `DATABASE_URL`: PostgreSQL connection string * `REDIS_URL`: Redis connection string * `CLICKHOUSE_URL`: ClickHouse connection string * `API_URL`: API endpoint URL (set via `SERVICE_FQDN_OPAPI`) * `DASHBOARD_URL`: Dashboard URL (set via `SERVICE_FQDN_OPDASHBOARD`) * `COOKIE_SECRET`: Automatically generated secret * `ENCRYPTION_KEY`: Automatically generated key (shared by the API and worker) You can configure optional variables like `ALLOW_REGISTRATION`, `RESEND_API_KEY`, `OPENAI_API_KEY`, etc. through Coolify's environment variable interface. The compose template exposes the following inputs for [social login](/docs/self-hosting/social-login) and [Google Search Console](/docs/self-hosting/google-search-console); Coolify maps each one to the containers that need it: * `MAKINFORU_GOOGLE_CLIENT_ID`, `MAKINFORU_GOOGLE_CLIENT_SECRET`, `MAKINFORU_GOOGLE_REDIRECT_URI` * `MAKINFORU_GITHUB_CLIENT_ID`, `MAKINFORU_GITHUB_CLIENT_SECRET`, `MAKINFORU_GITHUB_REDIRECT_URI` * `MAKINFORU_GSC_GOOGLE_REDIRECT_URI` The redirect URIs use the API's FQDN, e.g. `https://your-domain.com/api/oauth/google/callback`. Updating MakinForU [#updating-makinforu] To update MakinForU in Coolify: 1. Navigate to your MakinForU service 2. Click **"Redeploy"** or **"Update"** 3. Coolify will pull the latest images and restart services Database migrations run automatically when the API service starts, so updates are seamless. Scaling [#scaling] You can scale the worker service in Coolify: 1. Navigate to your MakinForU service 2. Edit the `opworker` service configuration 3. Adjust the replica count 4. Save and redeploy Troubleshooting [#troubleshooting] Services Not Starting [#services-not-starting] 1. Check service logs in Coolify dashboard 2. Verify all environment variables are set correctly 3. Ensure your server has enough resources (RAM, disk space) Database Connection Issues [#database-connection-issues] 1. Verify the database service (`opdb`) is running 2. Check that `DATABASE_URL` is correctly formatted 3. Review database logs in Coolify SSL Certificate Issues [#ssl-certificate-issues] If SSL certificates aren't being issued: 1. Verify your domain DNS is pointing to Coolify 2. Check Coolify's SSL/TLS settings 3. Review Coolify logs for Let's Encrypt errors Health Check Failures [#health-check-failures] If health checks are failing: 1. Check service logs for errors 2. Verify all dependencies are running 3. Increase health check timeout if needed Using Your Own Database [#using-your-own-database] If you want to use an external PostgreSQL database: 1. Create a new PostgreSQL database in Coolify or use an external service 2. Update the `DATABASE_URL` environment variable in your MakinForU service 3. Update `DATABASE_URL_DIRECT` to match 4. Redeploy the service The same applies to Redis and ClickHouse if you want to use external services. Backup and Restore [#backup-and-restore] Backup [#backup] Coolify provides built-in backup functionality: 1. Navigate to your database service (`opdb`) 2. Configure backup settings 3. Set up backup schedule 4. Backups will be stored according to your Coolify configuration Manual Backup [#manual-backup] You can also create manual backups: 1. Use Coolify's terminal access 2. Export the database: ```bash docker exec opdb pg_dump -U postgres makinforu-db > backup.sql ``` Restore [#restore] To restore from a backup: 1. Use Coolify's terminal access 2. Restore the database: ```bash docker exec -i opdb psql -U postgres makinforu-db < backup.sql ``` Next Steps [#next-steps] * [Configure email settings](/docs/self-hosting/self-hosting#e-mail) for password resets and invitations * [Set up AI integration](/docs/self-hosting/self-hosting#ai-integration) for the analytics assistant * [Configure SDK](/docs/self-hosting/self-hosting#always-use-correct-api-url) to track events from your applications * [Enable social login](/docs/self-hosting/social-login) with Google or GitHub * [Connect Google Search Console](/docs/self-hosting/google-search-console) for SEO data Additional Resources [#additional-resources] * [Coolify Documentation](https://coolify.io/docs) * [Coolify Services Directory](https://coolify.io/docs/services) * [MakinForU on Coolify](https://coolify.io/docs/services/makinforu) --- ## Deploy with Docker Compose URL: https://panel.makinforu.com/docs/self-hosting/deploy-docker-compose This guide will help you deploy MakinForU using Docker Compose. This method gives you full control over your deployment and is perfect for self-hosting on a VPS or dedicated server. Prerequisites [#prerequisites] * A VPS or server (Docker and Node will be installed automatically if needed) * At least 2GB RAM (4GB+ recommended) * Domain name pointing to your server (optional but recommended) * Basic knowledge of command line πŸ™‹β€β™‚οΈ This should work on any system. The setup script will install Docker and Node if they're not already installed. Quick Start [#quick-start] Clone the Repository [#clone-the-repository] Clone the MakinForU repository and navigate to the self-hosting directory: ```bash git clone https://github.com/deviljoker1911-beep/makinforu-panel.git cd makinforu-panel/self-hosting ``` Run the Setup Script [#run-the-setup-script] The setup script will guide you through the configuration process. It will: 1. Install Node.js (if you accept and it's not already installed) 2. Install Docker (if you accept and it's not already installed) 3. Run an interactive quiz/wizard that asks questions about your setup > Setup takes 30s to 2 minutes depending on your VPS ```bash ./setup ``` The wizard will ask you questions about: * Your domain name * Database configuration * Email settings (optional) * AI integration (optional) * Registration settings ⚠️ If the `./setup` script fails to run, you can do it manually: 1. Install Docker 2. Install Node.js 3. Install npm 4. Run `npm run quiz` inside the self-hosting folder Start the Services [#start-the-services] After the setup is complete, start all MakinForU services: ```bash ./start ``` This will start all required services: * **op-db**: PostgreSQL database * **op-kv**: Redis cache * **op-ch**: ClickHouse analytics database * **op-api**: MakinForU API server * **op-dashboard**: MakinForU dashboard (frontend) * **op-worker**: Background worker for processing events Verify Installation [#verify-installation] Check that all containers are running: ```bash docker compose ps ``` All services should show as "healthy" or "running". You can also check the logs: ```bash docker compose logs -f ``` Or use the provided logs script: ```bash ./logs ``` Once all services are healthy, you can access MakinForU at your configured domain (or `http://your-server-ip` if you haven't configured a domain). Configuration [#configuration] Environment Variables [#environment-variables] The setup wizard will configure most environment variables automatically. You can manually edit the `.env` file in the `self-hosting` directory if needed. For a complete reference of all available environment variables, see the [Environment Variables documentation](/docs/self-hosting/environment-variables). If you change the `.env` file, you need to restart the services for the changes to take effect. Use `./stop` and `./start` or `docker compose restart`. The generated `.env` also contains commented-out blocks for optional features. Uncomment and fill them in to enable [social login with Google or GitHub](/docs/self-hosting/social-login) and the [Google Search Console integration](/docs/self-hosting/google-search-console). Using Custom Docker Images [#using-custom-docker-images] If you want to use specific image versions, edit the `docker-compose.yml` file and update the image tags: ```yaml op-api: image: makinforu/makinforu-panel-api:2.0.0 # Specify version ``` Scaling Workers [#scaling-workers] To scale the worker service, set the `OP_WORKER_REPLICAS` environment variable: ```bash OP_WORKER_REPLICAS=3 docker compose up -d ``` Or edit the `docker-compose.yml` file: ```yaml op-worker: deploy: replicas: 3 ``` Managing Your Deployment [#managing-your-deployment] MakinForU comes with several utility scripts to help manage your deployment. All scripts should be run from within the `self-hosting` directory. Basic Operations [#basic-operations] ```bash ./start # Start all MakinForU services ./stop # Stop all MakinForU services ./logs # View real-time logs from all services ``` View Logs [#view-logs] View logs from all services: ```bash ./logs ``` Or using Docker Compose directly: ```bash docker compose logs -f ``` View logs from a specific service: ```bash docker compose logs -f op-api ``` Stop Services [#stop-services] Stop all services: ```bash ./stop ``` Or using Docker Compose: ```bash docker compose down ``` Stop services but keep volumes (data persists): ```bash docker compose stop ``` Restart Services [#restart-services] Restart all services: ```bash ./stop && ./start ``` Or using Docker Compose: ```bash docker compose restart ``` Restart a specific service: ```bash docker compose restart op-api ``` Rebuild Services [#rebuild-services] Rebuild and restart a specific service: ```bash ./rebuild op-dashboard ``` Update MakinForU [#update-makinforu] To update to the latest version, use the update script: ```bash ./update ``` This script will: 1. Pull the latest changes from the repository 2. Pull the latest Docker images 3. Restart all services If you don't have the `./update` script, you can manually update: ```bash git pull docker compose pull docker compose up -d ``` Always backup your data before updating. The database migrations will run automatically when the API container starts. Also read any changes in the [changelog](/docs/self-hosting/changelog) and apply them to your instance. Backup and Restore [#backup-and-restore] Backup [#backup] Backup your PostgreSQL database: ```bash docker compose exec op-db pg_dump -U postgres postgres > backup.sql ``` Backup volumes: ```bash docker run --rm -v makinforu_op-db-data:/data -v $(pwd):/backup alpine tar czf /backup/db-backup.tar.gz /data ``` Restore [#restore] Restore PostgreSQL database: ```bash docker compose exec -T op-db psql -U postgres postgres < backup.sql ``` Troubleshooting [#troubleshooting] Services Won't Start [#services-wont-start] 1. Check Docker and Docker Compose versions: ```bash docker --version docker compose version ``` 2. Check available disk space: ```bash df -h ``` 3. Check logs for errors: ```bash docker compose logs ``` Database Connection Issues [#database-connection-issues] If services can't connect to the database: 1. Verify the database is healthy: ```bash docker compose ps op-db ``` 2. Check database logs: ```bash docker compose logs op-db ``` 3. Verify `DATABASE_URL` in your `.env` file matches the service name `op-db` Port Conflicts [#port-conflicts] If ports 80 or 443 are already in use, you can: 1. Change the ports in `docker-compose.yml`: ```yaml ports: - "8080:80" - "8443:443" ``` 2. Or stop the conflicting service Health Check Failures [#health-check-failures] If health checks are failing: 1. Check if services are actually running: ```bash docker compose ps ``` 2. Increase health check timeout in `docker-compose.yml`: ```yaml healthcheck: interval: 30s timeout: 10s retries: 10 ``` Using Your Own Reverse Proxy [#using-your-own-reverse-proxy] If you want to use NGINX, Traefik, or another reverse proxy instead of the bundled Caddy β€” or put a TLS terminator upstream of Caddy β€” see the dedicated [Reverse proxy setup](/docs/self-hosting/reverse-proxy) guide. It covers the working NGINX config, the `Connection: upgrade` SSR-break gotcha, and the WebSocket upgrade headers required for the Realtime view. Next Steps [#next-steps] * [Configure email settings](/docs/self-hosting/self-hosting#e-mail) for password resets and invitations * [Set up AI integration](/docs/self-hosting/self-hosting#ai-integration) for the analytics assistant * [Configure SDK](/docs/self-hosting/self-hosting#always-use-correct-api-url) to track events from your applications * [Enable social login](/docs/self-hosting/social-login) with Google or GitHub * [Connect Google Search Console](/docs/self-hosting/google-search-console) for SEO data --- ## Deploy with Dokploy URL: https://panel.makinforu.com/docs/self-hosting/deploy-dokploy [Dokploy](https://dokploy.com) is an open-source, self-hosted platform for deploying applications. MakinForU can be deployed on Dokploy using the Docker Compose template, with some specific configuration requirements. ⚠️ **Important**: The Dokploy template requires specific configuration that differs from Coolify. Make sure to follow all steps carefully, especially the environment variables and domain configuration. ⚠️ **Important**: We have an open issue on dokploy [https://github.com/Dokploy/templates/issues/292](https://github.com/Dokploy/templates/issues/292) and hoping it will be resolved. Prerequisites [#prerequisites] * A Dokploy instance installed and running * A server with at least 2GB RAM (4GB+ recommended) * Domain name configured in Dokploy Quick Start [#quick-start] Deploy MakinForU Template [#deploy-makinforu-template] 1. Log in to your Dokploy dashboard 2. Navigate to your project 3. Click **"New Application"** or **"Deploy"** 4. Select **"Docker Compose"** or search for **"MakinForU"** template 5. Select the MakinForU template Configure Domain Names [#configure-domain-names] Configure your domain names in Dokploy: 1. Set up the main domain for the dashboard (e.g., `analytics.yourdomain.com`) 2. Configure the API domain - this should be the **same domain** as the dashboard, with the API path forwarded to `/api` The API and dashboard use the same domain. The API service has a forward path to `/api`, so make sure to configure this correctly. Configure Environment Variables [#configure-environment-variables] Edit the `.env` file or environment variables in Dokploy. You **must** set these environment variables: ```bash # Required: Set these to your actual domain API_URL=https://yourdomain.com/api DASHBOARD_URL=https://yourdomain.com # Database Configuration (automatically set by Dokploy) MAKINFORU_POSTGRES_DB=makinforu-db SERVICE_USER_POSTGRES=postgres SERVICE_PASSWORD_POSTGRES= SERVICE_PASSWORD_REDIS= # Optional Configuration MAKINFORU_ALLOW_REGISTRATION=false MAKINFORU_ALLOW_INVITATION=true RESEND_API_KEY=your-resend-api-key MAKINFORU_EMAIL_SENDER=noreply@yourdomain.com ``` ⚠️ **Critical**: Unlike Coolify, Dokploy does not support `SERVICE_FQDN_*` variables. You **must** hardcode `API_URL` and `DASHBOARD_URL` with your actual domain values. Configure API Service Domain Settings [#configure-api-service-domain-settings] In Dokploy, configure the API service domain: 1. Go to the `op-api` service configuration 2. Set up the domain configuration: ```toml [[config.domains]] serviceName = "op-api" port = 3000 host = "${api_domain}" ``` 3. **Important**: Check the **"Strip external path"** checkbox for the API service The "Strip external path" option is crucial! Without it, the API will receive incorrect paths when requests are forwarded from `/api`. Deploy [#deploy] 1. Review all configuration 2. Click **"Deploy"** or **"Save"** 3. Wait for all services to start and become healthy Monitor the deployment logs to ensure all services start correctly. Verify Installation [#verify-installation] Once deployment is complete: 1. Check that all services are running: * `op-api` - API server * `op-dashboard` - Dashboard (frontend) * `op-worker` - Background worker * `op-db` - PostgreSQL database * `op-kv` - Redis cache * `op-ch` - ClickHouse database 2. Access your dashboard at your configured domain 3. Try creating an account to verify the API is working correctly If you're using Cloudflare in front of Dokploy, remember to purge the Cloudflare cache after making changes to ensure updated resources are served. Configuration Details [#configuration-details] Required Environment Variables [#required-environment-variables] For Dokploy, you **must** hardcode these variables (unlike Coolify, Dokploy doesn't support `SERVICE_FQDN_*` variables): * `API_URL` - Full API URL (e.g., `https://analytics.example.com/api`) * `DASHBOARD_URL` - Full Dashboard URL (e.g., `https://analytics.example.com`) Dokploy automatically sets: * `MAKINFORU_POSTGRES_DB` - PostgreSQL database name * `SERVICE_USER_POSTGRES` - PostgreSQL username * `SERVICE_PASSWORD_POSTGRES` - PostgreSQL password (auto-generated) * `SERVICE_PASSWORD_REDIS` - Redis password (auto-generated) For a complete reference of all available environment variables, see the [Environment Variables documentation](/docs/self-hosting/environment-variables). Domain Configuration [#domain-configuration] The API and dashboard services share the same domain: * **Dashboard**: Serves the frontend at the root path (`/`) * **API**: Serves the API at `/api` path **Important settings for the API service:** * Domain: Same as dashboard domain * Path: `/api` * **Strip external path**: βœ… **Must be checked** This ensures that when requests come to `/api/*`, the path is correctly forwarded to the API service without the `/api` prefix. Troubleshooting [#troubleshooting] API Requests Not Working [#api-requests-not-working] If API requests fail after deployment: 1. **Verify environment variables**: ```bash # Check that API_URL is set correctly docker exec env | grep API_URL docker exec env | grep API_URL ``` 2. **Check "Strip external path" setting**: * Go to API service configuration in Dokploy * Ensure "Strip external path" is **checked** 3. **Verify domain configuration**: * API service should have path `/api` * Dashboard service should be at root `/` Account Creation Not Working [#account-creation-not-working] If account creation fails: 1. Check API logs: ```bash # In Dokploy, view logs for op-api service ``` 2. Verify `API_URL` matches your domain: * Should be `https://yourdomain.com/api` * Not `http://localhost:3000` or similar 3. Check that the API service is accessible: ```bash curl https://yourdomain.com/api/healthcheck ``` Cloudflare Cache Issues [#cloudflare-cache-issues] If you're using Cloudflare in front of Dokploy: 1. After deploying or updating, purge Cloudflare cache 2. This ensures updated resources are served immediately 3. You can do this from Cloudflare dashboard or API Database Connection Issues [#database-connection-issues] If services can't connect to databases: 1. Verify database services are running: * `op-db` (PostgreSQL) * `op-kv` (Redis) * `op-ch` (ClickHouse) 2. Check environment variables are set: ```bash docker exec env | grep DATABASE_URL ``` 3. Verify service names match in docker-compose: * Database service names: `op-db`, `op-kv`, `op-ch` * These should match the hostnames in connection strings Docker Compose Structure [#docker-compose-structure] The MakinForU template includes these services: * **op-api**: MakinForU API server * **op-dashboard**: MakinForU dashboard (frontend) * **op-worker**: Background worker for processing events * **op-db**: PostgreSQL database * **op-kv**: Redis cache * **op-ch**: ClickHouse analytics database Differences from Coolify [#differences-from-coolify] The Dokploy template differs from Coolify in these ways: 1. **Environment Variables**: * Dokploy does not support `SERVICE_FQDN_*` variables * Must hardcode `API_URL` and `DASHBOARD_URL` 2. **Domain Configuration**: * Must manually configure domain paths * Must enable "Strip external path" for API service 3. **Service Discovery**: * Uses standard Docker Compose service names * No automatic FQDN resolution Updating MakinForU [#updating-makinforu] To update MakinForU in Dokploy: 1. Pull the latest images: ```bash docker compose pull ``` 2. Restart services: ```bash docker compose up -d ``` Or use Dokploy's UI to restart services. Next Steps [#next-steps] * [Configure email settings](/docs/self-hosting/self-hosting#e-mail) for password resets and invitations * [Set up AI integration](/docs/self-hosting/self-hosting#ai-integration) for the analytics assistant * [Configure SDK](/docs/self-hosting/self-hosting#always-use-correct-api-url) to track events from your applications Additional Resources [#additional-resources] * [Dokploy Documentation](https://docs.dokploy.com/docs/core) * [MakinForU GitHub Issue #292](https://github.com/Dokploy/templates/issues/292) - Discussion about Dokploy deployment --- ## Deploy on Kubernetes URL: https://panel.makinforu.com/docs/self-hosting/deploy-kubernetes MakinForU can be deployed on Kubernetes using the community-maintained Helm chart. This allows you to run MakinForU in a scalable, production-ready Kubernetes environment. The Helm chart is maintained by the community and available on [Artifact Hub](https://artifacthub.io/packages/helm/makinforu/makinforu). Prerequisites [#prerequisites] * Kubernetes 1.19+ * Helm 3.0+ * `kubectl` configured to access your cluster * At least 2GB RAM per node (4GB+ recommended) * Persistent volume support (if using self-hosted databases) Quick Start [#quick-start] Add the Helm Repository [#add-the-helm-repository] Add the MakinForU Helm repository: ```bash helm repo add makinforu https://yashGoyal40.github.io/makinforu helm repo update ``` Download Default Values [#download-default-values] Download the default values file to customize your configuration: ```bash helm show values makinforu/makinforu > my-values.yaml ``` ⚠️ **IMPORTANT**: Before installing, you **MUST** configure the required values in `values.yaml`. The chart includes placeholder values (marked with `<>`) that will cause the installation to fail if not properly configured. Configure Required Values [#configure-required-values] Edit `my-values.yaml` and configure the following **required** values: 1. **Ingress Configuration**: ```yaml ingress: enabled: true type: standard # or "httpproxy" for Contour fqdn: your-domain.com # Replace with your actual domain standard: tlsSecretName: makinforu-tls ``` 2. **Application URLs**: ```yaml config: apiUrl: "https://your-domain.com/api" dashboardUrl: "https://your-domain.com" googleRedirectUri: "https://your-domain.com/api/oauth/google/callback" ``` 3. **Cookie Secret** (generate with `openssl rand -base64 32`): ```yaml secrets: cookieSecret: "YOUR_GENERATED_SECRET_HERE" ``` 4. **PostgreSQL Configuration** (choose one): * **Option A**: External PostgreSQL (recommended for production) ```yaml postgresql: enabled: false externalPostgresql: host: "postgres.example.com" port: 5432 user: "makinforu" password: "your-secure-password" database: "makinforu" schema: public ``` * **Option B**: Self-hosted PostgreSQL ```yaml postgresql: enabled: true user: postgres password: "your-secure-password" database: postgres persistence: size: 20Gi ``` Install MakinForU [#install-makinforu] Install MakinForU with your configured values: ```bash helm install my-makinforu makinforu/makinforu \ --version 0.1.0 \ --namespace makinforu \ --create-namespace \ -f my-values.yaml ``` Or override specific values directly: ```bash helm install my-makinforu makinforu/makinforu \ --version 0.1.0 \ --namespace makinforu \ --create-namespace \ --set ingress.fqdn=your-domain.com \ --set config.apiUrl=https://your-domain.com/api \ --set secrets.cookieSecret=$(openssl rand -base64 32) ``` Verify Installation [#verify-installation] Check that all pods are running: ```bash kubectl get pods -n makinforu ``` You should see pods for: * API server (`op-api`) * Dashboard (`op-dashboard`) * Worker (`op-worker`) * PostgreSQL (if using self-hosted) * Redis (if using self-hosted) * ClickHouse (if using self-hosted) Check the status: ```bash kubectl get all -n makinforu ``` Access Your Dashboard [#access-your-dashboard] Once all pods are running, access MakinForU at your configured domain. The ingress will route traffic to the dashboard service. If you need to test locally, you can port-forward: ```bash kubectl port-forward svc/op-dashboard 3000:80 -n makinforu ``` Then access MakinForU at `http://localhost:3000`. Configuration [#configuration] Required Configuration [#required-configuration] The following values **MUST** be configured before installation: | Configuration | Required | Placeholder | Description | | -------------------------- | -------------- | ------------------- | ----------------------------- | | `ingress.fqdn` | βœ… Yes | `` | Your domain name | | `ingress.*.tlsSecretName` | βœ… Yes | `` | TLS certificate secret name | | `config.apiUrl` | βœ… Yes | `` | Full API URL | | `config.dashboardUrl` | βœ… Yes | `` | Full Dashboard URL | | `config.googleRedirectUri` | βœ… Yes | `` | OAuth callback URL | | `secrets.cookieSecret` | βœ… Yes | `CHANGE_ME_...` | Session encryption key | | `externalPostgresql.*` | ⚠️ If external | `` | PostgreSQL connection details | Complete Example Configuration [#complete-example-configuration] Here's a minimal example configuration file (`my-values.yaml`) with all required values: ```yaml title="my-values.yaml" # Ingress Configuration ingress: enabled: true type: standard # or "httpproxy" for Contour fqdn: analytics.example.com standard: tlsSecretName: makinforu-tls # Application URLs config: apiUrl: "https://analytics.example.com/api" dashboardUrl: "https://analytics.example.com" googleRedirectUri: "https://analytics.example.com/api/oauth/google/callback" # Cookie Secret (generate with: openssl rand -base64 32) secrets: cookieSecret: "YOUR_GENERATED_SECRET_HERE" # PostgreSQL - Using External Database postgresql: enabled: false externalPostgresql: host: "postgres.example.com" port: 5432 user: "makinforu" password: "your-secure-password" database: "makinforu" schema: public ``` Optional Configuration [#optional-configuration] The Helm chart maps environment variables to Helm values. For a complete reference of all available environment variables and their descriptions, see the [Environment Variables documentation](/docs/self-hosting/environment-variables). Email Configuration [#email-configuration] Enable email functionality (password resets, invitations, etc.): ```yaml secrets: resendApiKey: "re_xxxxxxxxxxxxx" # Your Resend API key emailSender: "noreply@your-domain.com" # Verified sender email ``` Get your Resend API key from [resend.com](https://resend.com). Make sure to verify your sender email domain. AI Features [#ai-features] Enable the in-app AI chat assistant by setting one or both provider API keys: ```yaml secrets: openaiApiKey: "sk-xxxxxxxxxxxxx" # For GPT-4.1 / 4.1 mini / 5.4 mini anthropicApiKey: "sk-ant-xxxxxxxxxxxxx" # For Claude Haiku 4.5 / Sonnet 4.6 / Opus 4.6 ``` The chat's model picker only shows models whose provider has a key configured β€” set either or both. Without any key, the chat drawer still opens but shows a setup hint. Google OAuth [#google-oauth] Enable Google OAuth login: ```yaml secrets: googleClientId: "xxxxx.apps.googleusercontent.com" googleClientSecret: "GOCSPX-xxxxxxxxxxxxx" ``` Set up Google OAuth in [Google Cloud Console](https://console.cloud.google.com). Add authorized redirect URI: `https://your-domain.com/api/oauth/google/callback`. The full walkthrough is in the [social login guide](/docs/self-hosting/social-login). The chart does not currently have values for GitHub login or the [Google Search Console integration](/docs/self-hosting/google-search-console); those need `GITHUB_*`, `GSC_GOOGLE_REDIRECT_URI` and `ENCRYPTION_KEY` on the API (and worker) pods. Redis Configuration [#redis-configuration] Redis is enabled by default and deployed within Kubernetes. To use an external Redis instance: ```yaml redis: enabled: false externalRedis: host: "redis.example.com" port: 6379 ``` ClickHouse Configuration [#clickhouse-configuration] ClickHouse is enabled by default and deployed within Kubernetes. To use an external ClickHouse instance: ```yaml clickhouse: enabled: false externalClickhouse: host: "clickhouse.example.com" port: 8123 database: makinforu ``` Application Components [#application-components] Enable/disable individual components: ```yaml api: enabled: true replicas: 1 dashboard: enabled: true replicas: 1 worker: enabled: true replicas: 1 ``` Resource Limits [#resource-limits] Adjust resource requests and limits: ```yaml api: resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "2Gi" cpu: "2000m" ``` Updating MakinForU [#updating-makinforu] To upgrade to a newer version: ```bash helm repo update helm upgrade my-makinforu makinforu/makinforu \ --version \ --namespace makinforu \ -f my-values.yaml ``` Replace `` with the desired version number (e.g., `0.1.1`). Managing Your Deployment [#managing-your-deployment] View Logs [#view-logs] View logs from specific deployments: ```bash # API logs kubectl logs -f deployment/op-api -n makinforu # Dashboard logs kubectl logs -f deployment/op-dashboard -n makinforu # Worker logs kubectl logs -f deployment/op-worker -n makinforu ``` Restart Services [#restart-services] Restart a specific deployment: ```bash kubectl rollout restart deployment/op-api -n makinforu kubectl rollout restart deployment/op-dashboard -n makinforu kubectl rollout restart deployment/op-worker -n makinforu ``` Scale Services [#scale-services] Scale services on the fly: ```bash kubectl scale deployment/op-worker --replicas=3 -n makinforu ``` Or update your values file and upgrade: ```yaml worker: replicas: 3 ``` ```bash helm upgrade my-makinforu makinforu/makinforu -f my-values.yaml -n makinforu ``` Check Services [#check-services] View all services: ```bash kubectl get svc -n makinforu ``` Check ConfigMap and Secrets [#check-configmap-and-secrets] Verify configuration: ```bash kubectl get configmap makinforu-config -n makinforu -o yaml kubectl get secret makinforu-secrets -n makinforu -o yaml ``` Ingress Configuration [#ingress-configuration] Standard Ingress (NGINX/Traefik) [#standard-ingress-nginxtraefik] ```yaml ingress: enabled: true type: standard fqdn: makinforu.your-domain.com standard: tlsSecretName: makinforu-tls annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/ssl-redirect: "true" ``` HTTPProxy (Contour) [#httpproxy-contour] ```yaml ingress: enabled: true type: httpproxy fqdn: makinforu.your-domain.com httpproxy: tlsSecretName: makinforu-tls ``` Troubleshooting [#troubleshooting] Pods Not Starting [#pods-not-starting] 1. Check pod status: ```bash kubectl describe pod -n makinforu ``` 2. Check events: ```bash kubectl get events --sort-by='.lastTimestamp' -n makinforu ``` 3. Check logs: ```bash kubectl logs -n makinforu ``` Database Connection Issues [#database-connection-issues] 1. Verify database pods are running (if using self-hosted): ```bash kubectl get pods -n makinforu | grep postgres ``` 2. Check database service: ```bash kubectl get svc -n makinforu | grep postgres ``` 3. Test database connection: ```bash kubectl exec -it deployment/op-api -n makinforu -- env | grep DATABASE_URL ``` Configuration Issues [#configuration-issues] If pods are failing due to configuration: 1. Verify all required values are set: ```bash helm get values my-makinforu -n makinforu ``` 2. Check for placeholder values: ```bash helm get values my-makinforu -n makinforu | grep "<" ``` 3. Ensure secrets are properly set: ```bash kubectl get secret makinforu-secrets -n makinforu -o yaml ``` Ingress Not Working [#ingress-not-working] 1. Check ingress status: ```bash kubectl get ingress -n makinforu kubectl describe ingress -n makinforu ``` 2. Verify ingress controller is running: ```bash kubectl get pods -n ingress-nginx # For NGINX # or kubectl get pods -n projectcontour # For Contour ``` 3. Check DNS configuration Backup and Restore [#backup-and-restore] Backup PostgreSQL [#backup-postgresql] If using self-hosted PostgreSQL: ```bash kubectl exec -it -n makinforu -- \ pg_dump -U postgres makinforu > backup.sql ``` Or use a Kubernetes CronJob for automated backups. Restore PostgreSQL [#restore-postgresql] Restore from backup: ```bash kubectl exec -i -n makinforu -- \ psql -U postgres makinforu < backup.sql ``` Uninstalling [#uninstalling] To uninstall MakinForU: ```bash helm uninstall my-makinforu --namespace makinforu ``` ⚠️ **Warning**: This will delete all resources including persistent volumes. Make sure to backup your data before uninstalling! To keep persistent volumes: ```bash # Delete the release but keep PVCs helm uninstall my-makinforu --namespace makinforu # Manually delete PVCs if needed kubectl delete pvc -l app.kubernetes.io/name=makinforu -n makinforu ``` Next Steps [#next-steps] * [Configure email settings](/docs/self-hosting/self-hosting#e-mail) for password resets and invitations * [Set up AI integration](/docs/self-hosting/self-hosting#ai-integration) for the analytics assistant * [Configure SDK](/docs/self-hosting/self-hosting#always-use-correct-api-url) to track events from your applications Additional Resources [#additional-resources] * [Helm Chart on Artifact Hub](https://artifacthub.io/packages/helm/makinforu/makinforu) * [Kubernetes Documentation](https://kubernetes.io/docs/) * [Helm Documentation](https://helm.sh/docs/) --- ## Environment Variables URL: https://panel.makinforu.com/docs/self-hosting/environment-variables This page documents all environment variables used by MakinForU. Variables are organized by category. Most variables are optional and have sensible defaults. For deployment-specific configuration, see the [deployment guides](/docs/self-hosting/deploy-docker-compose). Database & Storage [#database--storage] DATABASE_URL [#database_url] **Type**: `string` **Required**: Yes **Default**: None PostgreSQL connection string for the main database. **Example**: ```bash DATABASE_URL=postgres://user:password@localhost:5432/makinforu?schema=public ``` DATABASE_URL_DIRECT [#database_url_direct] **Type**: `string` **Required**: No **Default**: Same as `DATABASE_URL` Direct PostgreSQL connection string (bypasses connection pooling). Used for migrations and administrative operations. **Example**: ```bash DATABASE_URL_DIRECT=postgres://user:password@localhost:5432/makinforu?schema=public ``` DATABASE_URL_REPLICA [#database_url_replica] **Type**: `string` **Required**: No **Default**: Same as `DATABASE_URL` Read replica connection string for read-heavy operations. If not set, uses the main database. **Example**: ```bash DATABASE_URL_REPLICA=postgres://user:password@replica-host:5432/makinforu?schema=public ``` REDIS_URL [#redis_url] **Type**: `string` **Required**: Yes **Default**: `redis://localhost:6379` Redis connection string for caching and queue management. **Example**: ```bash REDIS_URL=redis://localhost:6379 # With password REDIS_URL=redis://:password@localhost:6379 ``` CLICKHOUSE_URL [#clickhouse_url] **Type**: `string` **Required**: Yes **Default**: `http://localhost:8123/makinforu` ClickHouse HTTP connection URL for analytics data storage. **Example**: ```bash CLICKHOUSE_URL=http://localhost:8123/makinforu ``` CLICKHOUSE_CLUSTER [#clickhouse_cluster] **Type**: `boolean` **Required**: No **Default**: `false` Enable ClickHouse cluster mode. Set to `true` or `1` if using a ClickHouse cluster. **Example**: ```bash CLICKHOUSE_CLUSTER=true ``` CLICKHOUSE_SETTINGS [#clickhouse_settings] **Type**: `string` (JSON) **Required**: No **Default**: `{}` Additional ClickHouse settings as a JSON object. **Example**: ```bash CLICKHOUSE_SETTINGS='{"max_execution_time": 300}' ``` CLICKHOUSE_SETTINGS_REMOVE_CONVERT_ANY_JOIN [#clickhouse_settings_remove_convert_any_join] **Type**: `boolean` **Required**: No **Default**: `false` Remove `convert_any_join` from ClickHouse settings. Used for compatibility with certain ClickHouse versions. This needs to be set if you use any clickhouse version below 25! Application URLs [#application-urls] API_URL [#api_url] **Type**: `string` **Required**: Yes **Default**: None Public API URL exposed to the browser. Used by the dashboard frontend and API service. **Example**: ```bash API_URL=https://analytics.example.com/api ``` API_URL_SSR [#api_url_ssr] **Type**: `string` **Required**: No **Default**: Same as `API_URL` Internal API URL used **only** for the dashboard's server-side rendering (SSR) requests. Set this when the dashboard server can't resolve or reach the public `API_URL` and needs an internal address instead β€” for example a Docker Compose service name or a Kubernetes cluster-internal address. The browser always keeps using the public `API_URL`; only requests made from the dashboard server during SSR use `API_URL_SSR`. If unset, SSR falls back to `API_URL`. **Example**: ```bash # Docker Compose: reach the API service directly over the internal network API_URL_SSR=http://api:3000 # Kubernetes: cluster-internal service DNS API_URL_SSR=http://makinforu-api.default.svc.cluster.local:3000 ``` This only affects the dashboard's server-side requests. It does not change the API service itself, CORS, or any browser-facing URLs β€” those still use `API_URL`. DASHBOARD_URL [#dashboard_url] **Type**: `string` **Required**: Yes **Default**: None Public dashboard URL exposed to the browser. Used by the dashboard frontend and API service. **Example**: ```bash DASHBOARD_URL=https://analytics.example.com ``` API_CORS_ORIGINS [#api_cors_origins] **Type**: `string` (comma-separated) **Required**: No **Default**: None Additional CORS origins allowed for API requests. Comma-separated list of origins. **Example**: ```bash API_CORS_ORIGINS=https://app.example.com,https://another-app.com ``` Authentication & Security [#authentication--security] COOKIE_SECRET [#cookie_secret] **Type**: `string` **Required**: Yes **Default**: None Secret key for encrypting session cookies. Generate a secure random string (32+ characters). **Example**: ```bash # Generate with: openssl rand -base64 32 COOKIE_SECRET=your-random-secret-here ``` Never use the default value in production! Always generate a unique secret. ENCRYPTION_KEY [#encryption_key] **Type**: `string` (64 hexadecimal characters) **Required**: For [Google Search Console](/docs/self-hosting/google-search-console), two-factor authentication (TOTP) and object-store exports **Default**: None AES-256 key used for everything MakinForU encrypts at rest: Google Search Console tokens, users' 2FA secrets and S3/GCS export credentials. Must be exactly 32 bytes encoded as 64 hex characters, and must be identical on the API and worker services. The Docker Compose setup script generates one for you. **Example**: ```bash # Generate with: openssl rand -hex 32 ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ``` Rotating this key makes every existing ciphertext unreadable. Search Console connections must be re-authorised, 2FA must be re-enrolled and export credentials re-entered. Back it up together with your database. COOKIE_TLDS [#cookie_tlds] **Type**: `string` (comma-separated) **Required**: No **Default**: None Custom multi-part TLDs for cookie domain handling. Use this when deploying on domains with public suffixes that aren't recognized by default (e.g., `.my.id`, `.web.id`, `.co.id`). **Example**: ```bash # For domains like abc.my.id COOKIE_TLDS=my.id # Multiple TLDs COOKIE_TLDS=my.id,web.id,co.id ``` This is required when using domain suffixes that are public suffixes (like `.co.uk`). Without this, the browser will reject authentication cookies. Common examples include Indonesian domains (`.my.id`, `.web.id`, `.co.id`). CUSTOM_COOKIE_DOMAIN [#custom_cookie_domain] **Type**: `string` **Required**: No **Default**: None Override the automatic cookie domain detection and set a specific domain for authentication cookies. Useful when proxying the API through your main domain or when you need precise control over cookie scope. **Example**: ```bash # Set cookies only on the main domain CUSTOM_COOKIE_DOMAIN=.example.com # Set cookies on a specific subdomain CUSTOM_COOKIE_DOMAIN=.app.example.com ``` When set, this completely bypasses the automatic domain parsing logic. The cookie will always be set as secure. Include a leading dot (`.`) to allow the cookie to be shared across subdomains. DEMO_USER_ID [#demo_user_id] **Type**: `string` **Required**: No **Default**: None User ID for demo mode. When set, creates a demo session for testing. **Example**: ```bash DEMO_USER_ID=user_1234567890 ``` ALLOW_REGISTRATION [#allow_registration] **Type**: `boolean` **Required**: No **Default**: `false` (after first user is created) Allow new user registrations. Set to `true` to enable public registration. **Example**: ```bash ALLOW_REGISTRATION=true ``` Registration is automatically disabled after the first user is created. Set this to `true` to re-enable it. ALLOW_INVITATION [#allow_invitation] **Type**: `boolean` **Required**: No **Default**: `true` Allow user invitations. Set to `false` to disable invitation functionality. **Example**: ```bash ALLOW_INVITATION=false ``` AI Features [#ai-features] The in-app AI chat supports **OpenAI** and **Anthropic** models. Set one or both provider keys on the API service β€” the model picker in the chat UI automatically shows only the models whose provider has a key configured. If neither is set, the chat drawer still opens but shows setup instructions instead of suggestions. Available models: * **OpenAI** β€” `GPT-4.1`, `GPT-4.1 mini`, `GPT-5.4 mini` * **Anthropic** β€” `Claude Haiku 4.5`, `Claude Sonnet 4.6`, `Claude Opus 4.6` The AI assistant is optional. Without `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` set, every other MakinForU feature continues to work normally. OPENAI_API_KEY [#openai_api_key] **Type**: `string` **Required**: No **Default**: None OpenAI API key. When set, the OpenAI models appear in the chat model picker. **Example**: ```bash OPENAI_API_KEY=sk-your-openai-api-key-here ``` OPENAI_BASE_URL [#openai_base_url] **Type**: `string` **Required**: No **Default**: OpenAI default API base URL Override the OpenAI API base URL. Useful for proxies, gateways, Azure-compatible endpoints, or self-hosted OpenAI-compatible providers. **Example**: ```bash OPENAI_BASE_URL=https://your-openai-compatible-endpoint.example.com/v1 ``` OPENAI_PROJECT [#openai_project] **Type**: `string` **Required**: No **Default**: None Optional OpenAI project identifier to send with requests. **Example**: ```bash OPENAI_PROJECT=proj_1234567890 ``` OPENAI_ORGANIZATION [#openai_organization] **Type**: `string` **Required**: No **Default**: None Optional OpenAI organization identifier to send with requests. **Example**: ```bash OPENAI_ORGANIZATION=org_1234567890 ``` ANTHROPIC_API_KEY [#anthropic_api_key] **Type**: `string` **Required**: No **Default**: None Anthropic API key. When set, the Claude models appear in the chat model picker. **Example**: ```bash ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here ``` ANTHROPIC_BASE_URL [#anthropic_base_url] **Type**: `string` **Required**: No **Default**: Anthropic default API base URL Override the Anthropic API base URL. Useful for proxies, gateways, or Anthropic-compatible endpoints. **Example**: ```bash ANTHROPIC_BASE_URL=https://your-anthropic-endpoint.example.com ``` ANTHROPIC_TOKEN [#anthropic_token] **Type**: `string` **Required**: No **Default**: None Optional auth token sent to the Anthropic provider in addition to the API key. Use this only if your Anthropic-compatible gateway requires it. **Example**: ```bash ANTHROPIC_TOKEN=your-auth-token ``` ANTHROPIC_VERSION [#anthropic_version] **Type**: `string` **Required**: No **Default**: Provider default Override the Anthropic API version header sent with requests. **Example**: ```bash ANTHROPIC_VERSION=2023-06-01 ``` Email [#email] RESEND_API_KEY [#resend_api_key] **Type**: `string` **Required**: No **Default**: None Resend API key for sending transactional emails (password resets, invitations, etc.). **Example**: ```bash RESEND_API_KEY=re_xxxxxxxxxxxxx ``` Get your API key from [resend.com](https://resend.com). Make sure to verify your sender email domain. EMAIL_SENDER [#email_sender] **Type**: `string` **Required**: No **Default**: `hello@makinforu.com` Email address used as the sender for transactional emails. Applies to both Resend and SMTP transports. **Example**: ```bash EMAIL_SENDER=noreply@yourdomain.com ``` SMTP_HOST [#smtp_host] **Type**: `string` **Required**: No **Default**: None SMTP server hostname. When set, MakinForU uses SMTP to send emails instead of Resend. Takes priority over `RESEND_API_KEY` if both are configured. **Example**: ```bash SMTP_HOST=smtp.example.com ``` SMTP_PORT [#smtp_port] **Type**: `number` **Required**: No **Default**: `587` SMTP server port. **Example**: ```bash SMTP_PORT=587 ``` SMTP_SECURE [#smtp_secure] **Type**: `boolean` **Required**: No **Default**: `false` Use TLS for the SMTP connection. Set to `true` when using port `465`. **Example**: ```bash SMTP_SECURE=true ``` SMTP_USER [#smtp_user] **Type**: `string` **Required**: No **Default**: None SMTP authentication username. Leave unset if your server does not require authentication. **Example**: ```bash SMTP_USER=smtp-user@example.com ``` SMTP_PASS [#smtp_pass] **Type**: `string` **Required**: No **Default**: None SMTP authentication password. **Example**: ```bash SMTP_PASS=your-smtp-password ``` Set `SMTP_HOST` to enable SMTP. If both `SMTP_HOST` and `RESEND_API_KEY` are present, SMTP takes priority. If neither is set, emails are logged to the console (useful for development). In case of using Resend, the sender email must be verified in your Resend account. If `SMTP_HOST` is set and an SMTP send attempt fails, the system will **not** automatically fall back to `RESEND_API_KEY` β€” the email will be silently dropped. To use Resend instead, unset `SMTP_HOST`. OAuth & Integrations [#oauth--integrations] GOOGLE_CLIENT_ID [#google_client_id] **Type**: `string` **Required**: No **Default**: None OAuth client ID from Google Cloud Console. Enables [Sign in with Google](/docs/self-hosting/social-login#google) and is also used by the [Google Search Console integration](/docs/self-hosting/google-search-console). Set it on the API, and on the worker if you use Search Console. **Example**: ```bash GOOGLE_CLIENT_ID=123456789012-abcdefghijklmnop.apps.googleusercontent.com ``` GOOGLE_CLIENT_SECRET [#google_client_secret] **Type**: `string` **Required**: No (required together with `GOOGLE_CLIENT_ID`) **Default**: None OAuth client secret matching `GOOGLE_CLIENT_ID`. Only the API and worker need it. **Example**: ```bash GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx ``` GOOGLE_REDIRECT_URI [#google_redirect_uri] **Type**: `string` **Required**: For Google sign-in **Default**: None Full callback URL for Google sign-in: `${API_URL}/oauth/google/callback`. Must exactly match an authorized redirect URI on the Google OAuth client. It is not derived from `API_URL`, so set it explicitly. See [Social login](/docs/self-hosting/social-login). **Example**: ```bash GOOGLE_REDIRECT_URI=https://analytics.example.com/api/oauth/google/callback ``` GSC_GOOGLE_REDIRECT_URI [#gsc_google_redirect_uri] **Type**: `string` **Required**: For the Google Search Console integration **Default**: None Full callback URL for connecting Google Search Console: `${API_URL}/gsc/callback`. Must be registered as an authorized redirect URI on the same Google OAuth client as `GOOGLE_REDIRECT_URI`. Only the API needs it. See [Google Search Console](/docs/self-hosting/google-search-console). **Example**: ```bash GSC_GOOGLE_REDIRECT_URI=https://analytics.example.com/api/gsc/callback ``` GITHUB_CLIENT_ID [#github_client_id] **Type**: `string` **Required**: No **Default**: None Client ID of a GitHub OAuth App. Enables [Sign in with GitHub](/docs/self-hosting/social-login#github). Only the API needs it. **Example**: ```bash GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx ``` GITHUB_CLIENT_SECRET [#github_client_secret] **Type**: `string` **Required**: No (required together with `GITHUB_CLIENT_ID`) **Default**: None Client secret of the GitHub OAuth App. Only the API needs it. **Example**: ```bash GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` GITHUB_REDIRECT_URI [#github_redirect_uri] **Type**: `string` **Required**: For GitHub sign-in **Default**: None Full callback URL for GitHub sign-in: `${API_URL}/oauth/github/callback`. Must exactly match the authorization callback URL of the GitHub OAuth App. It is not derived from `API_URL`, so set it explicitly. **Example**: ```bash GITHUB_REDIRECT_URI=https://analytics.example.com/api/oauth/github/callback ``` The dashboard only shows the social login buttons and the Search Console settings when the API has both the client ID and the redirect URI for that feature. The dashboard itself needs none of these variables. SLACK_CLIENT_ID [#slack_client_id] **Type**: `string` **Required**: No **Default**: None Slack OAuth client ID for Slack integration. **Example**: ```bash SLACK_CLIENT_ID=1234567890.1234567890 ``` SLACK_CLIENT_SECRET [#slack_client_secret] **Type**: `string` **Required**: No **Default**: None Slack OAuth client secret for Slack integration. **Example**: ```bash SLACK_CLIENT_SECRET=your-slack-client-secret ``` SLACK_OAUTH_REDIRECT_URL [#slack_oauth_redirect_url] **Type**: `string` **Required**: No **Default**: None Slack OAuth redirect URL. Must match the redirect URI configured in your Slack app. **Example**: ```bash SLACK_OAUTH_REDIRECT_URL=https://analytics.example.com/api/integrations/slack/callback ``` SLACK_STATE_SECRET [#slack_state_secret] **Type**: `string` **Required**: No **Default**: None Secret for signing Slack OAuth state parameter. **Example**: ```bash SLACK_STATE_SECRET=your-state-secret ``` Self-hosting [#self-hosting] SELF_HOSTED [#self_hosted] **Type**: `boolean` **Required**: No **Default**: `false` Enable self-hosted mode. Set to `true` or `1` to enable self-hosting features. Used by both the dashboard frontend and API service. **Example**: ```bash SELF_HOSTED=true ``` Worker & Queue [#worker--queue] WORKER_PORT [#worker_port] **Type**: `number` **Required**: No **Default**: `3000` Port for the worker service to listen on. **Example**: ```bash WORKER_PORT=3000 ``` DISABLE_BULLBOARD [#disable_bullboard] **Type**: `boolean` **Required**: No **Default**: `false` Disable BullMQ board UI. Set to `true` or `1` to disable the queue monitoring dashboard. **Example**: ```bash DISABLE_BULLBOARD=true ``` DISABLE_WORKERS [#disable_workers] **Type**: `boolean` **Required**: No **Default**: `false` Disable all worker processes. Set to `true` or `1` to disable background job processing. **Example**: ```bash DISABLE_WORKERS=true ``` ENABLED_QUEUES [#enabled_queues] **Type**: `string` (comma-separated) **Required**: No **Default**: All queues enabled Comma-separated list of queue names to enable. Available queues: `events`, `events_kafka`, `sessions`, `cron`, `notification`, `import`, `insights`, `gsc`, `cohortCompute`. **Example**: ```bash ENABLED_QUEUES=events,sessions,cron ``` Enable `events_kafka` to run the Kafka-based events consumer alongside (or instead of) the default `events` queue. See the [Event Streaming (Kafka)](#event-streaming-kafka) section for the full configuration. A common high-throughput layout is to run one worker pool with `ENABLED_QUEUES=events_kafka` and another pool with everything else. EVENT_JOB_CONCURRENCY [#event_job_concurrency] **Type**: `number` **Required**: No **Default**: `10` Number of concurrent event processing jobs per worker. **Example**: ```bash EVENT_JOB_CONCURRENCY=20 ``` GSC_CONCURRENCY [#gsc_concurrency] **Type**: `number` **Required**: No **Default**: `5` Number of [Google Search Console](/docs/self-hosting/google-search-console) sync/backfill jobs a worker runs in parallel. Only relevant when the integration is enabled; requires `gsc` (and `cron`) in `ENABLED_QUEUES` if that variable is set. **Example**: ```bash GSC_CONCURRENCY=2 ``` EVENT_BLOCKING_TIMEOUT_SEC [#event_blocking_timeout_sec] **Type**: `number` **Required**: No **Default**: `1` Blocking timeout in seconds for event queue workers. **Example**: ```bash EVENT_BLOCKING_TIMEOUT_SEC=2 ``` EVENTS_GROUP_QUEUES_SHARDS [#events_group_queues_shards] **Type**: `number` **Required**: No **Default**: `1` Number of shards for the events group queue. Increase for better performance with high event volume. **Example**: ```bash EVENTS_GROUP_QUEUES_SHARDS=4 ``` QUEUE_CLUSTER [#queue_cluster] **Type**: `boolean` **Required**: No **Default**: `false` Enable Redis cluster mode for queues. When enabled, queue names are wrapped with `{}` for Redis cluster sharding. **Example**: ```bash QUEUE_CLUSTER=true ``` ORDERING_DELAY_MS [#ordering_delay_ms] **Type**: `number` **Required**: No **Default**: `100` Delay in milliseconds to hold events for correct ordering when events arrive out of order. **Example**: ```bash ORDERING_DELAY_MS=200 ``` Should not exceed 500ms. Higher values may cause delays in event processing. AUTO_BATCH_MAX_WAIT_MS [#auto_batch_max_wait_ms] **Type**: `number` **Required**: No **Default**: `0` (disabled) Maximum wait time in milliseconds for auto-batching events. Experimental feature. **Example**: ```bash AUTO_BATCH_MAX_WAIT_MS=100 ``` ⚠️ **Experimental**: This feature is experimental and not used in production. Do not use unless you have a good understanding of the implications and specific performance requirements. AUTO_BATCH_SIZE [#auto_batch_size] **Type**: `number` **Required**: No **Default**: `0` (disabled) Batch size for auto-batching events. Experimental feature. **Example**: ```bash AUTO_BATCH_SIZE=100 ``` ⚠️ **Experimental**: This feature is experimental and not used in production. Do not use unless you have a good understanding of the implications and specific performance requirements. Event Streaming (Kafka) [#event-streaming-kafka] MakinForU can optionally ingest events through a Kafka-compatible broker (Kafka itself, Redpanda, etc.) instead of the default Redis-backed events queue. This is useful at high throughput, where many consumer instances can process partitions in parallel. Routing is all-or-nothing: if `KAFKA_BROKERS` is set, every event is produced to the Kafka topic; otherwise all events stay on the default Redis-backed `events` queue. Set `ENABLED_QUEUES=events_kafka` on the worker(s) that should consume from Kafka. Connections are unauthenticated plaintext by default. For managed or production clusters, enable TLS with `KAFKA_SSL` and SASL authentication (`plain`, `scram-sha-256` or `scram-sha-512`) with the `KAFKA_SASL_*` variables below. The client speaks the Kafka wire protocol via [kafkajs](https://kafka.js.org/), so any Kafka-compatible broker works. The bundled `docker-compose.yml` uses [Redpanda](https://redpanda.com/) as a lightweight, single-binary option, but you can point `KAFKA_BROKERS` at any Kafka cluster. KAFKA_BROKERS [#kafka_brokers] **Type**: `string` (comma-separated) **Required**: Yes (to enable the Kafka path) **Default**: None Comma-separated list of Kafka broker addresses. When set, all events are produced to Kafka; when unset, the Kafka producer/consumer is disabled and all events fall back to the default Redis-backed queue. **Example**: ```bash KAFKA_BROKERS=redpanda:9092 # Multiple brokers KAFKA_BROKERS=kafka-1:9092,kafka-2:9092,kafka-3:9092 ``` KAFKA_SSL [#kafka_ssl] **Type**: `boolean` **Required**: No **Default**: `false` (automatically `true` when SASL credentials are set) Connect to the brokers over TLS. Set this to `true` for a TLS-only cluster without authentication. When `KAFKA_SASL_USERNAME`/`KAFKA_SASL_PASSWORD` are set, TLS is enabled automatically; set `KAFKA_SSL=false` explicitly to send SASL over plaintext (only sensible on a trusted internal network). **Example**: ```bash KAFKA_SSL=true ``` KAFKA_SSL_CA_PATH [#kafka_ssl_ca_path] **Type**: `string` **Required**: No **Default**: None (system trust store) Path to a PEM-encoded CA certificate (or bundle) to trust in addition to the system trust store. Use this when your brokers present certificates signed by a private or self-signed CA. Requires TLS to be enabled. **Example**: ```bash KAFKA_SSL_CA_PATH=/etc/makinforu/kafka-ca.pem ``` KAFKA_SSL_REJECT_UNAUTHORIZED [#kafka_ssl_reject_unauthorized] **Type**: `boolean` **Required**: No **Default**: `true` Whether to reject broker certificates that cannot be verified. Only set to `false` for local development against a self-signed broker; prefer `KAFKA_SSL_CA_PATH` in production. Requires TLS to be enabled. **Example**: ```bash KAFKA_SSL_REJECT_UNAUTHORIZED=false ``` KAFKA_SASL_USERNAME [#kafka_sasl_username] **Type**: `string` **Required**: No (required together with `KAFKA_SASL_PASSWORD` to enable SASL) **Default**: None SASL username. Setting a username without a password (or vice versa) is a configuration error and the process refuses to start. Credential values are never logged. **Example**: ```bash KAFKA_SASL_USERNAME=makinforu ``` KAFKA_SASL_PASSWORD [#kafka_sasl_password] **Type**: `string` **Required**: No (required together with `KAFKA_SASL_USERNAME` to enable SASL) **Default**: None SASL password. **Example**: ```bash KAFKA_SASL_PASSWORD=your-secret ``` KAFKA_SASL_MECHANISM [#kafka_sasl_mechanism] **Type**: `string` **Required**: No **Default**: `scram-sha-512` SASL mechanism to authenticate with. One of `plain`, `scram-sha-256` or `scram-sha-512` (case-insensitive). Any other value is rejected at startup, before a Kafka client is created. **Example**: ```bash # TLS + SASL/SCRAM-SHA-512 (the default when credentials are set) KAFKA_BROKERS=kafka-1.example.com:9093,kafka-2.example.com:9093 KAFKA_SASL_USERNAME=makinforu KAFKA_SASL_PASSWORD=your-secret # TLS + SASL/PLAIN with a private CA KAFKA_BROKERS=kafka.internal:9093 KAFKA_SASL_MECHANISM=plain KAFKA_SASL_USERNAME=makinforu KAFKA_SASL_PASSWORD=your-secret KAFKA_SSL_CA_PATH=/etc/makinforu/kafka-ca.pem ``` The producer runs in idempotent mode, so the SASL principal needs the `IdempotentWrite` cluster permission in addition to `Write`/`Describe` on the topic and `Read` on the consumer group. Managed Kafka providers usually grant this by default; on a locked-down cluster you may need to add it explicitly. KAFKA_EVENTS_TOPIC [#kafka_events_topic] **Type**: `string` **Required**: No **Default**: `events` Name of the Kafka topic used for incoming events. **Example**: ```bash KAFKA_EVENTS_TOPIC=makinforu-events ``` KAFKA_CONSUMER_GROUP [#kafka_consumer_group] **Type**: `string` **Required**: No **Default**: `makinforu-events` Kafka consumer group ID used by the worker. All worker replicas with the same group ID share partition assignments cooperatively. **Example**: ```bash KAFKA_CONSUMER_GROUP=makinforu-events ``` KAFKA_CLIENT_ID [#kafka_client_id] **Type**: `string` **Required**: No **Default**: `makinforu` Kafka client ID used by both producer and consumer. Shows up in broker logs and metrics. **Example**: ```bash KAFKA_CLIENT_ID=makinforu-prod ``` KAFKA_PARTITIONS_CONCURRENT [#kafka_partitions_concurrent] **Type**: `number` **Required**: No **Default**: `8` Maximum number of partitions a single consumer instance processes concurrently. Has no effect beyond the number of partitions actually assigned to this consumer. **Example**: ```bash KAFKA_PARTITIONS_CONCURRENT=12 ``` KAFKA_MIN_MESSAGES [#kafka_min_messages] **Type**: `number` **Required**: No **Default**: `1` Minimum number of messages the broker should accumulate before responding to a fetch request (each message is assumed to be \~1 KiB internally). Higher values create larger batches at the cost of slightly higher latency. **Example**: ```bash KAFKA_MIN_MESSAGES=16 ``` KAFKA_MAX_WAIT_MS [#kafka_max_wait_ms] **Type**: `number` **Required**: No **Default**: `500` Maximum time in milliseconds the broker waits to fulfil `KAFKA_MIN_MESSAGES` before returning a fetch response. Caps end-to-end latency under low traffic. **Example**: ```bash KAFKA_MAX_WAIT_MS=200 ``` KAFKA_MAX_MESSAGES_PER_PARTITION [#kafka_max_messages_per_partition] **Type**: `number` **Required**: No **Default**: `256` Maximum number of messages returned per partition per fetch request (each message is assumed to be \~1 KiB internally). Caps in-memory batch size during backlog recovery and bounds the worst-case time the consumer spends on a single batch. **Example**: ```bash KAFKA_MAX_MESSAGES_PER_PARTITION=512 ``` KAFKA_SESSION_TIMEOUT_MS [#kafka_session_timeout_ms] **Type**: `number` **Required**: No **Default**: `30000` Consumer group session timeout in milliseconds. The broker considers a consumer dead and rebalances its partitions if it does not heartbeat within this window. **Example**: ```bash KAFKA_SESSION_TIMEOUT_MS=45000 ``` KAFKA_HEARTBEAT_INTERVAL_MS [#kafka_heartbeat_interval_ms] **Type**: `number` **Required**: No **Default**: `3000` How often, in milliseconds, the consumer sends background heartbeats to the broker. Should be well below `KAFKA_SESSION_TIMEOUT_MS`. **Example**: ```bash KAFKA_HEARTBEAT_INTERVAL_MS=3000 ``` Buffers [#buffers] SESSION_BUFFER_BATCH_SIZE [#session_buffer_batch_size] **Type**: `number` **Required**: No **Default**: Buffer-specific default Batch size for session buffer operations. **Example**: ```bash SESSION_BUFFER_BATCH_SIZE=5000 ``` SESSION_BUFFER_CHUNK_SIZE [#session_buffer_chunk_size] **Type**: `number` **Required**: No **Default**: Buffer-specific default Chunk size for session buffer operations. **Example**: ```bash SESSION_BUFFER_CHUNK_SIZE=1000 ``` EVENT_BUFFER_BATCH_SIZE [#event_buffer_batch_size] **Type**: `number` **Required**: No **Default**: `4000` Batch size for event buffer operations. **Example**: ```bash EVENT_BUFFER_BATCH_SIZE=5000 ``` EVENT_BUFFER_CHUNK_SIZE [#event_buffer_chunk_size] **Type**: `number` **Required**: No **Default**: `1000` Chunk size for event buffer operations. **Example**: ```bash EVENT_BUFFER_CHUNK_SIZE=2000 ``` PROFILE_BUFFER_BATCH_SIZE [#profile_buffer_batch_size] **Type**: `number` **Required**: No **Default**: Buffer-specific default Batch size for profile buffer operations. **Example**: ```bash PROFILE_BUFFER_BATCH_SIZE=5000 ``` PROFILE_BUFFER_CHUNK_SIZE [#profile_buffer_chunk_size] **Type**: `number` **Required**: No **Default**: Buffer-specific default Chunk size for profile buffer operations. **Example**: ```bash PROFILE_BUFFER_CHUNK_SIZE=1000 ``` PROFILE_BUFFER_TTL_IN_SECONDS [#profile_buffer_ttl_in_seconds] **Type**: `number` **Required**: No **Default**: Buffer-specific default Time-to-live in seconds for profile buffer entries. **Example**: ```bash PROFILE_BUFFER_TTL_IN_SECONDS=3600 ``` BOT_BUFFER_BATCH_SIZE [#bot_buffer_batch_size] **Type**: `number` **Required**: No **Default**: Buffer-specific default Batch size for bot detection buffer operations. **Example**: ```bash BOT_BUFFER_BATCH_SIZE=1000 ``` Analytics Behavior [#analytics-behavior] FUNNEL_NON_STRICT_ORDERING [#funnel_non_strict_ordering] **Type**: `boolean` (`1` or `true`) **Required**: No **Default**: unset (strict ordering) Funnels use ClickHouse's `windowFunnel` in `strict_increase` mode by default: every step's timestamp must be strictly greater than the previous step's. Setting this variable switches funnels to non-strict ordering (`>=`), where steps sharing the same timestamp still count as an ordered sequence β€” matching how Mixpanel and Amplitude evaluate funnels. Useful for server-side senders, batched SDKs, or imported data with coarse timestamps. **Example**: ```bash FUNNEL_NON_STRICT_ORDERING=1 ``` Performance & Tuning [#performance--tuning] EVENT_LIST_MAX_LOOKBACK_DAYS [#event_list_max_lookback_days] **Type**: `number` (whole days, positive integer) **Required**: No **Default**: `1825` (5 years) Ceiling for the empty-result lookback on the event list. When a page's cursor window matches nothing, the window doubles and retries until it reaches this many days. With filters that no index can prune (e.g. a properties value) each retry is a full scan of its window, so large deployments can bound the worst case with a small, human-sized value. Invalid values (non-integer, zero, negative) keep the default. **Example**: ```bash EVENT_LIST_MAX_LOOKBACK_DAYS=7 ``` SESSION_LIST_MAX_LOOKBACK_DAYS [#session_list_max_lookback_days] **Type**: `number` (whole days, positive integer) **Required**: No **Default**: `365` Same ceiling for the session list, which has its own default. Invalid values keep the default. **Example**: ```bash SESSION_LIST_MAX_LOOKBACK_DAYS=7 ``` COHORT_QUERY_MEMORY_LIMIT_BYTES [#cohort_query_memory_limit_bytes] **Type**: `number` (bytes, positive integer) **Required**: No **Default**: unset (server defaults govern) Hard memory cap applied to property-cohort queries, which aggregate every profile row for a project and are the one cohort query that can outgrow the server's headroom. When set without `COHORT_QUERY_SPILL_BYTES`, the spill threshold is derived as a third of this limit so the query spills to disk before it can be killed. Unset (together with the spill variable) applies no per-query settings at all. Invalid values are ignored. **Example**: ```bash COHORT_QUERY_MEMORY_LIMIT_BYTES=1400000000 ``` COHORT_QUERY_SPILL_BYTES [#cohort_query_spill_bytes] **Type**: `number` (bytes, positive integer) **Required**: No **Default**: unset (derived as `COHORT_QUERY_MEMORY_LIMIT_BYTES / 3` when that is set) Point past which a property-cohort GROUP BY spills to disk instead of growing in memory. Must sit below the memory limit β€” a threshold at or above the kill limit means the query dies before it ever spills, so inverted pairs are re-derived. Invalid values are ignored. **Example**: ```bash COHORT_QUERY_SPILL_BYTES=314572800 ``` IMPORT_BATCH_SIZE [#import_batch_size] **Type**: `number` **Required**: No **Default**: `5000` Batch size for data import operations. **Example**: ```bash IMPORT_BATCH_SIZE=10000 ``` EVENT_PROPERTY_VALUE_AUTOCOMPLETE_LIMIT [#event_property_value_autocomplete_limit] **Type**: `number` (positive integer)\ **Required**: No\ **Default**: `500` Cap on distinct values returned per event property to the filter autocomplete. High-cardinality keys (ids, urls, tokens) can hold millions of distinct values; the most recent ones win. Invalid values keep the default. **Example**: ```bash EVENT_PROPERTY_VALUE_AUTOCOMPLETE_LIMIT=1000 ``` IP_HEADER_ORDER [#ip_header_order] **Type**: `string` (comma-separated) **Required**: No **Default**: See [default order](https://github.com/deviljoker1911-beep/makinforu-panel/blob/main/packages/common/server/get-client-ip.ts) Custom order of HTTP headers to check for client IP address. Useful when behind specific proxies or CDNs. **Example**: ```bash IP_HEADER_ORDER=cf-connecting-ip,x-real-ip,x-forwarded-for ``` The default order includes: `makinforu-client-ip`, `cf-connecting-ip`, `true-client-ip`, `x-client-ip`, `x-forwarded-for`, `x-real-ip`, and others. See the [source code](https://github.com/deviljoker1911-beep/makinforu-panel/blob/main/packages/common/server/get-client-ip.ts) for the complete default list. SHUTDOWN_GRACE_PERIOD_MS [#shutdown_grace_period_ms] **Type**: `number` **Required**: No **Default**: `5000` Grace period in milliseconds for graceful shutdown of services. **Example**: ```bash SHUTDOWN_GRACE_PERIOD_MS=10000 ``` API_PORT [#api_port] **Type**: `number` **Required**: No **Default**: `3000` Port for the API service to listen on. **Example**: ```bash API_PORT=3000 ``` API_HOST [#api_host] **Type**: `string` **Required**: No **Default**: `0.0.0.0` (production) / `localhost` (development) Host address for the API service to bind to. Set to `::` to enable IPv6 support (useful for platforms like Railway that use IPv6 for internal networking). **Example**: ```bash # Default IPv4 only API_HOST=0.0.0.0 # IPv6 (dual-stack, accepts both IPv4 and IPv6) API_HOST=:: ``` Use `API_HOST=::` when deploying on platforms like Railway where private networking requires IPv6. The `::` address enables dual-stack mode, accepting both IPv4 and IPv6 connections on most systems. Logging [#logging] LOG_LEVEL [#log_level] **Type**: `string` **Required**: No **Default**: `info` Logging level. Options: `error`, `warn`, `info`, `debug`. **Example**: ```bash LOG_LEVEL=debug ``` LOG_SILENT [#log_silent] **Type**: `boolean` **Required**: No **Default**: `false` Disable all logging output. Set to `true` to silence logs. **Example**: ```bash LOG_SILENT=true ``` LOG_PREFIX [#log_prefix] **Type**: `string` **Required**: No **Default**: None Prefix for log messages. Useful for identifying logs from different services. **Example**: ```bash LOG_PREFIX=api ``` HYPERDX_API_KEY [#hyperdx_api_key] **Type**: `string` **Required**: No **Default**: None HyperDX API key for sending logs to HyperDX for monitoring and analysis. **Example**: ```bash HYPERDX_API_KEY=your-hyperdx-api-key ``` Geo [#geo] MAXMIND_LICENSE_KEY [#maxmind_license_key] **Type**: `string` **Required**: No **Default**: None MaxMind GeoLite2 license key for downloading GeoIP databases. **Example**: ```bash MAXMIND_LICENSE_KEY=your-maxmind-license-key ``` Get your license key from [MaxMind](https://www.maxmind.com/en/accounts/current/license-key). Required for downloading GeoIP databases. Quick Reference [#quick-reference] Required Variables [#required-variables] For a basic self-hosted installation, these variables are required: * `DATABASE_URL` - PostgreSQL connection * `REDIS_URL` - Redis connection * `CLICKHOUSE_URL` - ClickHouse connection * `API_URL` - API endpoint URL * `DASHBOARD_URL` - Dashboard URL * `COOKIE_SECRET` - Session encryption secret Optional but Recommended [#optional-but-recommended] * `RESEND_API_KEY` or `SMTP_HOST` - For email features (pick one) * `EMAIL_SENDER` - Email sender address * `OPENAI_API_KEY` and/or `ANTHROPIC_API_KEY` - For the in-app AI chat assistant * `ENCRYPTION_KEY` - Required for Google Search Console, 2FA and exports * `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`, `GOOGLE_REDIRECT_URI` - [Sign in with Google](/docs/self-hosting/social-login) * `GITHUB_CLIENT_ID`, `GITHUB_CLIENT_SECRET`, `GITHUB_REDIRECT_URI` - [Sign in with GitHub](/docs/self-hosting/social-login) * `GSC_GOOGLE_REDIRECT_URI` - [Google Search Console integration](/docs/self-hosting/google-search-console) See Also [#see-also] * [Deploy with Docker Compose](/docs/self-hosting/deploy-docker-compose) * [Deploy with Coolify](/docs/self-hosting/deploy-coolify) * [Deploy with Dokploy](/docs/self-hosting/deploy-dokploy) * [Deploy on Kubernetes](/docs/self-hosting/deploy-kubernetes) --- ## Google Search Console URL: https://panel.makinforu.com/docs/self-hosting/google-search-console The Google Search Console (GSC) integration imports search performance data into MakinForU: clicks, impressions, CTR and average position per page and per query. Once enabled it powers the SEO page in each project (`///seo`), Search Console insights on page reports, and the `gsc_*` tools in the [MCP server](/docs/mcp#google-search-console) and the AI assistant. Enabling it is an instance-level setup (this page) plus a per-project connection that any project admin does from the project's settings under "Google Search". How it works [#how-it-works] 1. A project admin clicks "Connect Google Search Console" and is sent to Google with the read-only `webmasters.readonly` scope. 2. Google redirects back to the API with a code. The API exchanges it for an access token and a refresh token and stores both in PostgreSQL, encrypted with `ENCRYPTION_KEY`. 3. The admin picks which Search Console property to use. A backfill of the last 6 months starts immediately, in 14-day chunks. 4. Every night at 03:00 (worker time zone) the worker re-syncs a rolling 3-day window, because Google finalises data a couple of days late. 5. Data lands in the ClickHouse tables `gsc_daily`, `gsc_pages_daily` and `gsc_queries_daily`. Re-syncs overwrite earlier rows. There is no retention limit. A few views (keyword cannibalization, page and query detail drill-downs) query Google live from the API and cache the result for 4 hours, so both the API and the worker need the Google credentials and `ENCRYPTION_KEY`. Prerequisites [#prerequisites] You need a Google Cloud OAuth client. If you already set up [Google sign-in](/docs/self-hosting/social-login), reuse it. The integration uses the same `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET`. Google sign-in does not have to be enabled; the OAuth client alone is enough. You also need `ENCRYPTION_KEY` set on the API and worker. The Docker Compose setup script generates one. Other deployments must create it with `openssl rand -hex 32`. Finally, you need the public `API_URL` of your instance, e.g. `https://analytics.example.com/api`. Setup [#setup] Enable the Search Console API [#enable-the-search-console-api] In the [Google Cloud Console](https://console.cloud.google.com/), open "APIs & Services", "Library", search for "Google Search Console API" and click "Enable". Without this every request fails with `403 Google Search Console API has not been used in project ... before or it is disabled`. Configure the consent screen and scope [#configure-the-consent-screen-and-scope] Open "APIs & Services", "OAuth consent screen" (or "Google Auth Platform") and make sure the app exists. See the [Google sign-in guide](/docs/self-hosting/social-login#google) if you are starting from scratch. Under "Data access" (or "Scopes"), add `https://www.googleapis.com/auth/webmasters.readonly` ("View Search Console data for your verified sites"). Testing versus published matters here. While the consent screen is in Testing mode, Google expires refresh tokens after 7 days. The nightly sync will then fail with status `token_expired` and the project will need to be reconnected every week. For a stable connection, click "Publish app". `webmasters.readonly` is classed as a sensitive scope. Publishing an External app that requests it may trigger a request for Google's verification process. In practice this is only enforced when the app is used by many users outside your organisation. For an internal instance you can usually publish and keep using it, but Google will show an "unverified app" warning on the consent screen until verified. If everyone who will connect Search Console is in your Google Workspace, choose Internal as the user type. Internal apps never need verification and their refresh tokens do not expire after 7 days. Add the callback URL to the OAuth client [#add-the-callback-url-to-the-oauth-client] Open "APIs & Services", "Credentials", select your OAuth 2.0 client (or create a Web application client as described in the [sign-in guide](/docs/self-hosting/social-login#google)) and add another authorized redirect URI: ``` ${API_URL}/gsc/callback ``` For example `https://analytics.example.com/api/gsc/callback`. This is a different URL from the sign-in callback (`/oauth/google/callback`). Both can coexist on the same client. Add the environment variables [#add-the-environment-variables] ```bash title=".env" GOOGLE_CLIENT_ID=123456789012-abcdefghijklmnop.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx GSC_GOOGLE_REDIRECT_URI=https://analytics.example.com/api/gsc/callback ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef ``` `GSC_GOOGLE_REDIRECT_URI` must match the redirect URI registered in the previous step exactly. `ENCRYPTION_KEY` must be exactly 64 hexadecimal characters (32 bytes). Generate one with `openssl rand -hex 32`. Restart the services [#restart-the-services] Which services need which variables: | Variable | API | Worker | Dashboard | | ------------------------- | --- | ------------------------------------ | --------- | | `GOOGLE_CLIENT_ID` | yes | yes | no | | `GOOGLE_CLIENT_SECRET` | yes | yes | no | | `GSC_GOOGLE_REDIRECT_URI` | yes | no | no | | `ENCRYPTION_KEY` | yes | yes (must be identical to the API's) | no | The dashboard needs none of these. It asks the API whether the integration is configured. How you set them depends on how you deployed MakinForU. With the Docker Compose setup, add them to `self-hosting/.env` (all services read it) and run `docker compose up -d --force-recreate op-api op-worker`. With Coolify, the bundled template exposes `MAKINFORU_GOOGLE_CLIENT_ID`, `MAKINFORU_GOOGLE_CLIENT_SECRET` and `MAKINFORU_GSC_GOOGLE_REDIRECT_URI`, and generates `ENCRYPTION_KEY` for you. For any other setup, add them to the API and worker environment and restart both. Connect a project [#connect-a-project] In the dashboard, open the project's settings, go to the "Google Search" tab and click "Connect Google Search Console". Sign in with a Google account that has at least Restricted access to the property in Search Console, and grant the read-only permission. You are returned to the settings tab with a list of every property that account can see. Both domain properties (`sc-domain:example.com`) and URL-prefix properties (`https://example.com/`) are listed. Pick the one that matches the site the project tracks. A 6-month backfill starts as soon as you click "Connect property". The badge on the settings page shows its progress, and the SEO page fills in as chunks complete. Things to know [#things-to-know] Each project has at most one Search Console connection. Disconnecting keeps the data already imported into ClickHouse. Reconnecting to a different property overwrites overlapping dates. The refresh token belongs to the Google user who clicked "Connect". If that person loses access to the property, or leaves the organisation, syncing stops with `token_expired` and someone else must reconnect. Rotating `ENCRYPTION_KEY` breaks existing connections. Stored tokens can no longer be decrypted and every project must reconnect. The same key also protects two-factor secrets and export credentials, so treat it as permanent. If you restrict the worker with [`ENABLED_QUEUES`](/docs/self-hosting/environment-variables#enabled_queues), it must include both `gsc` (sync and backfill jobs) and `cron` (the nightly trigger). `GSC_CONCURRENCY` (default `5`) controls how many projects sync in parallel. Search Console allows thousands of requests per site per day. A 6-month backfill makes roughly 40 requests, and the nightly sync makes 3 per project. You are unlikely to hit limits unless you connect hundreds of projects. Google typically finalises data 2 to 3 days after the fact. MakinForU requests `dataState: all`, so recent days include preliminary numbers that later syncs correct. If the connect callback fails, MakinForU redirects to `/login?error=...&correlationId=...` even though you are still signed in. Navigate back to the project and search the API logs for the correlation ID to see the full error. Troubleshooting [#troubleshooting] | Symptom | Likely cause | | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Settings tab says "Google Search Console is not configured on this instance" | The API is missing `GOOGLE_CLIENT_ID` or `GSC_GOOGLE_REDIRECT_URI`, or was not restarted after you added them. | | Google shows `Error 400: redirect_uri_mismatch` | `GSC_GOOGLE_REDIRECT_URI` differs from the redirect URI registered on the OAuth client. Check `http` vs `https`, the `/api` prefix and trailing slashes. | | `/login?error=No refresh token returned from Google GSC OAuth` | Google only issues a refresh token on the first consent for a client, and MakinForU forces re-consent to work around that. If you still hit this, revoke MakinForU under [myaccount.google.com/permissions](https://myaccount.google.com/permissions) and connect again. | | `/login?error=Missing GSC OAuth cookies` or `GSC OAuth state mismatch` | The state cookies set before the redirect did not reach the API. The API and dashboard must share a registrable domain. See the cookie notes in the [social login guide](/docs/self-hosting/social-login#before-you-start). | | Connection shows "Authorization expired" (`token_expired`) | The refresh token was revoked or expired. Most often the consent screen is in Testing mode (7-day limit). Publish the app, then click "Reconnect". | | Sync status "error" with `Google Search Console API has not been used in project` | The Search Console API is not enabled in the Google Cloud project. Enable it and wait a minute. | | Sync status "error" with `403` / `User does not have sufficient permission for site` | The connected Google account lost access to the property in Search Console. | | Worker logs `ENCRYPTION_KEY environment variable is not set` or `must be 32 bytes (64 hex characters)` | `ENCRYPTION_KEY` is missing or malformed on the worker. It must be identical on the API and the worker. | | Worker logs `GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET is not set in this environment` | The worker has no Google credentials and cannot refresh tokens. Add them to the worker service. | | "No Search Console properties found for this Google account" | The Google account you signed in with has no properties in Search Console. Add the account as a user on the property (Search Console settings, "Users and permissions") and reconnect. | | SEO page is empty right after connecting | The backfill runs in the background in 14-day chunks. The settings page shows its status. If it stays on "pending", check that the worker is running and `ENABLED_QUEUES` includes `gsc`. | --- ## High volume setup URL: https://panel.makinforu.com/docs/self-hosting/high-volume The default Docker Compose setup works well for most deployments. When you start seeing high event throughput β€” thousands of events per second or dozens of worker replicas β€” a few things need adjusting. Connection pooling with PGBouncer [#connection-pooling-with-pgbouncer] PostgreSQL has a hard limit on the number of open connections. Each worker and API replica opens its own pool of connections, so the total can grow fast. Without pooling, you will start seeing `too many connections` errors under load. PGBouncer sits in front of PostgreSQL and maintains a small pool of real database connections, multiplexing many application connections on top of them. Add PGBouncer to docker-compose.yml [#add-pgbouncer-to-docker-composeyml] Add the `op-pgbouncer` service and update the `op-api` and `op-worker` dependencies: ```yaml op-pgbouncer: image: edoburu/pgbouncer:v1.25.1-p0 restart: always depends_on: op-db: condition: service_healthy environment: - DB_HOST=op-db - DB_PORT=5432 - DB_USER=postgres - DB_PASSWORD=postgres - DB_NAME=postgres - AUTH_TYPE=scram-sha-256 - POOL_MODE=transaction - MAX_CLIENT_CONN=1000 - DEFAULT_POOL_SIZE=20 - MIN_POOL_SIZE=5 - RESERVE_POOL_SIZE=5 healthcheck: test: ["CMD-SHELL", "PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres pgbouncer -c 'SHOW VERSION;' -q || exit 1"] interval: 10s timeout: 5s retries: 5 logging: driver: "json-file" options: max-size: "10m" max-file: "3" ``` Then update `op-api` and `op-worker` to depend on `op-pgbouncer` instead of `op-db`: ```yaml op-api: depends_on: op-pgbouncer: condition: service_healthy op-ch: condition: service_healthy op-kv: condition: service_healthy op-worker: depends_on: op-pgbouncer: condition: service_healthy op-api: condition: service_healthy ``` Update DATABASE_URL [#update-database_url] Prisma needs to know it is talking to a pooler. Point `DATABASE_URL` at `op-pgbouncer` and add `&pgbouncer=true`: ```bash # Before DATABASE_URL=postgresql://postgres:postgres@op-db:5432/postgres?schema=public # After DATABASE_URL=postgresql://postgres:postgres@op-pgbouncer:5432/postgres?schema=public&pgbouncer=true ``` Leave `DATABASE_URL_DIRECT` pointing at `op-db` directly, without the `pgbouncer=true` flag. Migrations use the direct connection and will not work through a transaction-mode pooler. ```bash DATABASE_URL_DIRECT=postgresql://postgres:postgres@op-db:5432/postgres?schema=public ``` PGBouncer runs in transaction mode. Prisma migrations and interactive transactions require a direct connection. Always set `DATABASE_URL_DIRECT` to the `op-db` address. Tuning the pool size [#tuning-the-pool-size] A rough rule: `DEFAULT_POOL_SIZE` should not exceed your PostgreSQL `max_connections` divided by the number of distinct database/user pairs. The PostgreSQL default is 100. If you raise `max_connections` in Postgres, you can raise `DEFAULT_POOL_SIZE` proportionally. *** Buffer tuning [#buffer-tuning] Events, sessions, and profiles flow through in-memory Redis buffers before being written to ClickHouse in batches. The defaults are conservative. Under high load you want larger batches to reduce the number of ClickHouse inserts and improve throughput. Event buffer [#event-buffer] The event buffer collects incoming events in Redis and flushes them to ClickHouse on a cron schedule. | Variable | Default | What it controls | | ------------------------------- | ------- | -------------------------------------------------------------------- | | `EVENT_BUFFER_BATCH_SIZE` | `4000` | How many events are read from Redis and sent to ClickHouse per flush | | `EVENT_BUFFER_CHUNK_SIZE` | `1000` | How many events are sent in a single ClickHouse insert call | | `EVENT_BUFFER_MICRO_BATCH_MS` | `10` | How long (ms) to accumulate events in memory before writing to Redis | | `EVENT_BUFFER_MICRO_BATCH_SIZE` | `100` | Max events to accumulate before forcing a Redis write | For high throughput, increase `EVENT_BUFFER_BATCH_SIZE` so each flush processes more events. Keep `EVENT_BUFFER_CHUNK_SIZE` at or below `EVENT_BUFFER_BATCH_SIZE`. ```bash EVENT_BUFFER_BATCH_SIZE=10000 EVENT_BUFFER_CHUNK_SIZE=2000 ``` Session buffer [#session-buffer] Sessions are updated on each event and flushed to ClickHouse separately. | Variable | Default | What it controls | | --------------------------- | ------- | ---------------------------- | | `SESSION_BUFFER_BATCH_SIZE` | `1000` | Events read per flush | | `SESSION_BUFFER_CHUNK_SIZE` | `1000` | Events per ClickHouse insert | ```bash SESSION_BUFFER_BATCH_SIZE=5000 SESSION_BUFFER_CHUNK_SIZE=2000 ``` Profile buffer [#profile-buffer] Profiles are merged with existing data before writing. The default batch size is small because each profile may require a ClickHouse lookup. | Variable | Default | What it controls | | ------------------------------- | ------- | ---------------------------------------- | | `PROFILE_BUFFER_BATCH_SIZE` | `200` | Profiles processed per flush | | `PROFILE_BUFFER_CHUNK_SIZE` | `1000` | Profiles per ClickHouse insert | | `PROFILE_BUFFER_TTL_IN_SECONDS` | `3600` | How long a profile stays cached in Redis | Raise `PROFILE_BUFFER_BATCH_SIZE` if profile processing is a bottleneck. Higher values mean fewer flushes but more memory used per flush. ```bash PROFILE_BUFFER_BATCH_SIZE=500 PROFILE_BUFFER_CHUNK_SIZE=1000 ``` *** Scaling ingestion [#scaling-ingestion] If the event queue is growing faster than workers can drain it, you have a few options. Start vertical before going horizontal. Each worker replica adds overhead: more Redis connections, more ClickHouse connections, more memory. Increasing concurrency on an existing replica is almost always cheaper and more effective than adding another one. Increase job concurrency (do this first) [#increase-job-concurrency-do-this-first] Each worker processes multiple jobs in parallel. The default is `10` per replica. ```bash EVENT_JOB_CONCURRENCY=20 ``` Raise this in steps and watch your queue depth. The limit is memory, not logic β€” values of `500`, `1000`, or even `2000+` are possible on hardware with enough RAM. Each concurrent job holds event data in memory, so monitor usage as you increase the value. Only add more replicas once concurrency alone stops helping. Add more worker replicas [#add-more-worker-replicas] If you have maxed out concurrency and the queue is still falling behind, add more replicas. In `docker-compose.yml`: ```yaml op-worker: deploy: replicas: 8 ``` Or at runtime: ```bash docker compose up -d --scale op-worker=8 ``` Shard the events queue [#shard-the-events-queue] **Experimental.** Queue sharding requires either a Redis Cluster or Dragonfly. Dragonfly has seen minimal testing and Redis Cluster has not been tested at all. Do not use this in production without validating it in your environment first. Redis is single-threaded, so a single queue instance can become the bottleneck at very high event rates. Queue sharding works around this by splitting the queue across multiple independent shards. Each shard can be backed by its own Redis instance, so the throughput scales with the number of instances rather than being capped by one core. Events are distributed across shards by project ID, so ordering within a project is preserved. ```bash EVENTS_GROUP_QUEUES_SHARDS=4 QUEUE_CLUSTER=true ``` Set `EVENTS_GROUP_QUEUES_SHARDS` before you have live traffic on the queue. Changing it while jobs are pending will cause those jobs to be looked up on the wrong shard and they will not be processed until the shard count is restored. Tune the ordering delay [#tune-the-ordering-delay] Events arriving out of order are held briefly before processing. The default is `100ms`. ```bash ORDERING_DELAY_MS=100 ``` Lowering this reduces latency but increases the chance of out-of-order writes to ClickHouse. The value should not exceed `500ms`. *** Putting it together [#putting-it-together] A starting point for a high-volume `.env`: ```bash # Route app traffic through PGBouncer DATABASE_URL=postgresql://postgres:postgres@op-pgbouncer:5432/postgres?schema=public&pgbouncer=true # Keep direct connection for migrations DATABASE_URL_DIRECT=postgresql://postgres:postgres@op-db:5432/postgres?schema=public # Event buffer EVENT_BUFFER_BATCH_SIZE=10000 EVENT_BUFFER_CHUNK_SIZE=2000 # Session buffer SESSION_BUFFER_BATCH_SIZE=5000 SESSION_BUFFER_CHUNK_SIZE=2000 # Profile buffer PROFILE_BUFFER_BATCH_SIZE=500 # Queue EVENTS_GROUP_QUEUES_SHARDS=4 EVENT_JOB_CONCURRENCY=20 ``` Then start with more workers: ```bash docker compose up -d --scale op-worker=8 ``` Monitor the Redis queue depth and ClickHouse insert latency as you tune. The right values depend on your hardware, event shape, and traffic pattern. --- ## Latest Docker Images URL: https://panel.makinforu.com/docs/self-hosting/latest-docker-images Running the latest build [#running-the-latest-build] Self-hosted MakinForU Panel pins its container images in `self-hosting/docker-compose.yml`. Stable releases are pinned by major tag (for example `:2`), but you can also run images built from the newest commits on `main`. MakinForU Panel is a modified AGPLv3 fork of OpenPanel. It does **not** use the upstream OpenPanel image registry, and the upstream supporter program does not apply here. Images come from your own registry, built from this repository. Where the images come from [#where-the-images-come-from] Commit builds are produced by the GitHub Actions workflow in `.github/workflows/docker-build.yml` and pushed to `ghcr.io/`. Tagged releases are built by `sh/docker-build` and pushed to Docker Hub as `makinforu/makinforu-panel-*`. The self-hosting scripts read that registry from the `MAKINFORU_REGISTRY` environment variable: ```bash export MAKINFORU_REGISTRY=ghcr.io/your-org # default: ghcr.io/makinforu ``` The resulting image names look like: ``` $MAKINFORU_REGISTRY/api:main- $MAKINFORU_REGISTRY/dashboard:main- $MAKINFORU_REGISTRY/worker:main- ``` If your registry is private, log in on the server first: ```bash echo "your_token" | docker login ghcr.io -u --password-stdin ``` Updating [#updating] Navigate to your self-hosting folder and run: ```bash ./get_latest_images apply ``` This script will: * Check that you're authenticated with the configured registry * Fetch the latest Git tags from the repository * Back up your `docker-compose.yml`, then update it with the new image tags You can also inspect what's available without applying anything: ```bash ./get_latest_images # Show latest tags ./get_latest_images --list # List all available tags ``` Then restart your services: ```bash ./restart ``` Quick update workflow [#quick-update-workflow] ```bash cd /path/to/self-hosting export MAKINFORU_REGISTRY=ghcr.io/your-org ./get_latest_images apply ./restart ``` Important notes [#important-notes] * **Stability**: builds from `main` are tested but may contain undiscovered bugs. Keep a backup strategy. * **Breaking changes**: check the [changelog](/docs/self-hosting/changelog) before updating. * **Support**: open an issue or a thread in [GitHub Discussions](https://github.com/deviljoker1911-beep/makinforu-panel/discussions). Need help? [#need-help] * [GitHub Discussions](https://github.com/deviljoker1911-beep/makinforu-panel/discussions) * Email [hello@makinforu.com](mailto:hello@makinforu.com) * [GitHub repository](https://github.com/deviljoker1911-beep/makinforu-panel) --- ## Migrating from Clerk URL: https://panel.makinforu.com/docs/self-hosting/migrating-from-clerk As of version 0.0.5, we have removed Clerk.com from MakinForU. This means that if you are upgrading from a previous version, you will need to export your users from Clerk and import them into MakinForU. Here is how you can do it. Before we start lets get the users from Clerk. Go to **Clerk > Configure > Settings > Export all users** and download the CSV file. This file will be used to import the users into MakinForU. Copy the csv file we downloaded from Clerk to your server: ```bash scp ./path/to/your/clerk-users.csv user@your-ip:users-dump.csv ``` SSH into your server: ```bash ssh user@your-ip ``` Pull the latest images, and restart the containers: ```bash docker compose pull docker compose down docker compose up -d ``` SSH into your server: ```bash ssh user@your-ip ``` Run the following command to copy the file to the MakinForU container: ```bash docker compose cp ./users-dump.csv op-api:/app/packages/db/code-migrations/users-dump.csv ``` Run the migration: ```bash docker compose exec -it op-api bash -c "cd /app/packages/db && pnpm migrate:deploy:code 2-accounts.ts" ``` --- ## Reverse proxy setup URL: https://panel.makinforu.com/docs/self-hosting/reverse-proxy MakinForU ships with a Caddy container that handles TLS and routing. It's two lines of config and Just Works. If you prefer NGINX, Traefik, HAProxy, or you're putting another TLS terminator in front of Caddy, this page covers how and β€” more importantly β€” what will break along the way. When do you need this? [#when-do-you-need-this] * **Replacing Caddy with NGINX/Traefik/HAProxy** β€” jump to [Your own reverse proxy](#your-own-reverse-proxy). * **Putting a TLS terminator upstream of the bundled Caddy** (cloud LB, edge VM) β€” see [TLS terminated upstream of Caddy](#tls-terminated-upstream-of-caddy). * **Just using the bundled Caddy** β€” you don't need this page. `./setup` + `./start` and you're done. How the bundled Caddy does it [#how-the-bundled-caddy-does-it] The bundled Caddy config is deliberately small. Everything else on this page is a translation of this into your proxy of choice: ```text title="self-hosting/caddy/Caddyfile.template" $DOMAIN_NAME {$SSL_CONFIG encode gzip handle_path /api* { reverse_proxy op-api:3000 } reverse_proxy /* op-dashboard:3000 } ``` That's it. Two rules: * `/api/*` β†’ `op-api:3000` with the `/api` prefix stripped * `/*` β†’ `op-dashboard:3000` **The API handles CORS itself** β€” your proxy doesn't need to add `Access-Control-*` headers, and there's no special `/track` route. Everything under `/api/` (including `/api/track`, `/api/live/*`, `/api/export`, …) hits the API container. Your own reverse proxy [#your-own-reverse-proxy] The simplest way to plug in your own proxy is to run it as another container on the same Docker network: comment out `op-proxy` in `docker-compose.yml`, add your proxy service, and target `op-api:3000` / `op-dashboard:3000` by service name β€” same as Caddy does. No host-port binding needed on the app containers. Every snippet below is a **template**, not a finished config. TLS, logging, rate limiting, body size limits, and whatever your deployment actually needs β€” that's on you. NGINX [#nginx] ```nginx # Forward Connection: upgrade only when the client asked for an upgrade. # Unconditional "upgrade" breaks the dashboard's SSR β€” see gotchas. map $http_upgrade $connection_upgrade { default keep-alive; websocket upgrade; } server { listen 443 ssl http2; server_name makinforu.example.com; # ssl_certificate / ssl_certificate_key β€” your TLS setup here # /api/* β†’ API (prefix stripped) location /api/ { proxy_pass http://op-api:3000/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 24h; # keep idle /api/live/* WebSockets alive } # /* β†’ dashboard location / { proxy_pass http://op-dashboard:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` Caddy does this in two lines. NGINX needs a lot more boilerplate to land on the same behaviour β€” the `map` block for conditional upgrades, explicit `X-Forwarded-*` headers, a long `proxy_read_timeout` for WebSockets. See [gotchas](#gotchas). Traefik [#traefik] ```yaml op-api: labels: - traefik.enable=true - traefik.http.routers.op-api.rule=Host(`makinforu.example.com`) && PathPrefix(`/api`) - traefik.http.middlewares.op-api-strip.stripprefix.prefixes=/api - traefik.http.routers.op-api.middlewares=op-api-strip - traefik.http.services.op-api.loadbalancer.server.port=3000 op-dashboard: labels: - traefik.enable=true - traefik.http.routers.op-dashboard.rule=Host(`makinforu.example.com`) - traefik.http.services.op-dashboard.loadbalancer.server.port=3000 ``` Traefik forwards WebSocket upgrades by default, so the Realtime view works without extra config. TLS terminated upstream of Caddy [#tls-terminated-upstream-of-caddy] If you're terminating TLS on a cloud load balancer or another VM and sending plain HTTP to the bundled Caddy, you'll hit mixed-content errors on the login page β€” the dashboard thinks it's on HTTP because the inner Caddy doesn't see the forwarded scheme. Fix on the Caddy side β€” tell it to trust the upstream's `X-Forwarded-*` headers: ``` { auto_https off admin off servers { trusted_proxies static private_ranges } } ``` `private_ranges` trusts RFC1918 addresses β€” the usual case for an LB sitting in the same VPC. Without this, Caddy ignores `X-Forwarded-Proto: https` from the upstream and the dashboard builds `http://` URLs. On the upstream terminator, forward the scheme: ``` X-Forwarded-Proto: https ``` And set the public URL in `.env` so the dashboard + API use the right scheme everywhere: ```bash title=".env" DASHBOARD_URL=https://makinforu.example.com API_URL=https://makinforu.example.com/api CORS_ORIGIN=https://makinforu.example.com ``` Gotchas [#gotchas] These are the things that actually bite. The ones that make you stare at "Something went wrong" for an hour. `Connection: upgrade` can break dashboard SSR [#connection-upgrade-can-break-dashboard-ssr] Unconditionally forwarding `Connection: upgrade` on every request β€” a common copy-paste for "add WebSocket support" β€” breaks the dashboard's server-side rendering with an opaque `fetch failed`. **Why:** the dashboard does server-side tRPC calls to the API during SSR. Node's `undici` rejects any `fetch()` carrying `Connection: upgrade` with `UND_ERR_INVALID_ARG` β†’ `TypeError: fetch failed` β†’ browser shows "Something went wrong / fetch failed". **Fix:** only forward the upgrade header when the client asked for one. The NGINX snippet above does this with the `map $http_upgrade $connection_upgrade` block. Caddy and Traefik handle this correctly out of the box. WebSocket upgrade required for `/api/live/*` [#websocket-upgrade-required-for-apilive] The Realtime view and live notifications use WebSockets at `/api/live/events/:projectId` and `/api/live/notifications/:projectId`. If your proxy drops the `Upgrade` / `Connection` headers, the page loads fine (from an SSR snapshot) but never updates. DevTools β†’ Network β†’ WS will show the failed connections. For NGINX you need both: ```nginx proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 24h; ``` The long `proxy_read_timeout` stops the proxy from closing idle connections between events. CORS is handled by the API β€” don't re-add it in the proxy [#cors-is-handled-by-the-api--dont-re-add-it-in-the-proxy] The API handles CORS dynamically per-path (dashboard routes are origin-locked; `/api/track` is permissive). If your proxy also adds `Access-Control-Allow-Origin: *` headers, browsers will see two headers and reject the response. Let the API do it. Mixed-content behind an upstream TLS terminator [#mixed-content-behind-an-upstream-tls-terminator] See [TLS terminated upstream of Caddy](#tls-terminated-upstream-of-caddy). Short version: `trusted_proxies` on the inner Caddy, `X-Forwarded-Proto: https` from the upstream, `DASHBOARD_URL=https://...` in `.env`. SDK `apiUrl` must match what the browser loads [#sdk-apiurl-must-match-what-the-browser-loads] Whatever your final public URL is, the SDK `apiUrl` has to match β€” including the `/api` path: ```ts new MakinForU({ apiUrl: 'https://makinforu.example.com/api', clientId: 'YOUR_CLIENT_ID', }); ``` See [Always use correct API URL](/docs/self-hosting/self-hosting#always-use-correct-api-url). --- ## Get started with self-hosting URL: https://panel.makinforu.com/docs/self-hosting/self-hosting Instructions [#instructions] Prerequisites [#prerequisites] * VPS of any kind (only tested on Ubuntu 24.04) * [Hetzner](https://www.hetzner.com/cloud) is a good default, but any VPS provider works β€” pick one in the region your data needs to stay in. * πŸ™‹β€β™‚οΈ This should work on any system if you have pre-installed docker, node and pnpm Quickstart [#quickstart] ```bash git clone https://github.com/deviljoker1911-beep/makinforu-panel.git cd makinforu-panel/self-hosting && ./setup # After setup is complete run `./start` to start MakinForU ``` Clone [#clone] Clone the repository to your VPS ```bash git clone https://github.com/deviljoker1911-beep/makinforu-panel.git ``` Upstream OpenPanel keeps a dedicated `self-hosting` branch that lags behind `main`. This fork does not β€” `main` is what you deploy. Run the setup script [#run-the-setup-script] The setup script will do 3 things 1. Install node (if you accept) 2. Install docker (if you accept) 3. Execute a node script that will ask some questions about your setup > Setup takes 30s to 2 minutes depending on your VPS ```bash cd makinforu-panel/self-hosting ./setup ``` ⚠️ If the `./setup` script fails to run, you can do it manually. 1. Install docker 2. Install node 3. Install npm 4. Run the `npm run quiz` script inside the self-hosting folder Start πŸš€ [#start-] Run the `./start` script located inside the self-hosting folder ```bash ./start ``` `./start` pulls the `makinforu/makinforu-panel-*` images from Docker Hub. Those are published from this repository's release pipeline β€” if you are running a fork, or before the first release, they will not exist and the pull fails. Build them from source instead: ```bash docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build ``` Set `export COMPOSE_FILE=docker-compose.yml:docker-compose.build.yml` so the `./restart`, `./update` and `./stop` scripts use the same pair. The build needs roughly 4 GB of free RAM and 10 GB of disk; on a smaller server, build elsewhere and push to a registry. Good to know [#good-to-know] Always use correct api url [#always-use-correct-api-url] When self-hosting you'll need to provide your api url when initializing the SDK. The path should be `/api` and the domain should be your domain. ```html title="index.html" ``` ```js title="op.ts" import { MakinForU } from '@makinforu/sdk'; const op = new MakinForU({ apiUrl: 'https://your-domain.com/api', // [!code highlight] clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, trackOutgoingLinks: true, trackAttributes: true, }); ``` E-mail [#e-mail] Some of MakinForU's features require e-mail. We use Resend as our transactional e-mail provider by default, but you can also use your own SMTP server. To enable email features, set the relevant environment variables described below. This is nothing that is required for the basic setup, but it is required for some features. Features that require e-mail: * Password reset * Invitations * more will be added over time Option A β€” Resend [#option-a--resend] Create an account on [resend.com](https://resend.com), verify your sender domain, and set the `RESEND_API_KEY` environment variable. ```bash title=".env" RESEND_API_KEY=re_xxxxxxxxxxxxx EMAIL_SENDER=noreply@yourdomain.com ``` Option B β€” SMTP [#option-b--smtp] If you prefer to use your own SMTP server, set `SMTP_HOST` and MakinForU will use it instead of Resend. ```bash title=".env" SMTP_HOST=smtp.example.com SMTP_PORT=587 SMTP_SECURE=false SMTP_USER=smtp-user@example.com SMTP_PASS=your-smtp-password EMAIL_SENDER=noreply@yourdomain.com ``` `SMTP_HOST` takes priority over `RESEND_API_KEY` if both are set. If neither is configured, emails are logged to the console instead of being sent. For email configuration details, see the [Environment Variables documentation](/docs/self-hosting/environment-variables#email). AI integration [#ai-integration] MakinForU includes an AI-powered analytics assistant that can help you analyze data, create reports, and answer questions about your analytics. It supports both OpenAI and Anthropic β€” set one or both keys and the model picker in the chat will only show models whose provider has a key configured. Supported Models [#supported-models] * **OpenAI** β€” `GPT-4.1`, `GPT-4.1 mini`, `GPT-5.4 mini` * **Anthropic** β€” `Claude Haiku 4.5`, `Claude Sonnet 4.6`, `Claude Opus 4.6` Configuration [#configuration] Add one or both of the following to your `.env` file in the `self-hosting` directory, then restart the API service: ```bash title=".env" # OpenAI β€” get a key at platform.openai.com/api-keys OPENAI_API_KEY=sk-your-openai-api-key-here # Anthropic β€” get a key at console.anthropic.com ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key-here ``` The AI assistant is optional. Without either key set, the chat drawer still opens but shows a setup hint β€” every other MakinForU feature continues to work normally. AI features will incur costs based on your usage and the model you choose. Monitor your API usage through your provider's dashboard to avoid unexpected charges. For complete AI configuration details, see the [Environment Variables documentation](/docs/self-hosting/environment-variables#ai-features). Managed Redis [#managed-redis] If you use a managed Redis service, you may need to set the `notify-keyspace-events` manually. Without this setting we won't be able to listen for expired keys which we use for calculating currently active visitors. > You will see a warning in the logs if this needs to be set manually. Registration / Invitations [#registration--invitations] By default registrations are disabled after the first user is created. You can change this by setting the `ALLOW_REGISTRATION` environment variable to `true`. ```bash title=".env" ALLOW_REGISTRATION=true ``` Invitations are enabled by default. You can also disable invitations by setting the `ALLOW_INVITATION` environment variable to `false`. ```bash title=".env" ALLOW_INVITATION=false ``` For a complete reference of all environment variables, see the [Environment Variables documentation](/docs/self-hosting/environment-variables). Helpful scripts [#helpful-scripts] MakinForU comes with several utility scripts to help manage your self-hosted instance: Basic Operations [#basic-operations] ```bash ./start # Start all MakinForU services ./stop # Stop all MakinForU services ./logs # View real-time logs from all services ``` Maintenance [#maintenance] ```bash ./rebuild # Rebuild and restart a specific service # Example: ./rebuild op-dashboard ``` Troubleshooting [#troubleshooting] ```bash ./danger_wipe_everything # ⚠️ Removes all containers, volumes, and data # Only use this if you want to start fresh! ``` The `danger_wipe_everything` script will delete all your MakinForU data including databases, configurations, and cached files. Use with extreme caution! All these scripts should be run from within the `self-hosting` directory. Make sure the scripts are executable (`chmod +x script-name` if needed). Updating [#updating] To grab the latest and greatest from MakinForU you should just run the `./update` script inside the self-hosting folder. If you don't have the `./update` script, you can run `git pull` and then `./update` Also read any changes in the [changelog](/docs/self-hosting/changelog) and apply them to your instance. --- ## Social login (Google & GitHub) URL: https://panel.makinforu.com/docs/self-hosting/social-login MakinForU supports signing in with Google and GitHub in addition to email and password. Both are optional. Email/password login always works, and the social login buttons only appear when the provider is configured. Enabling a provider takes three steps: 1. Create an OAuth application at the provider and register MakinForU's callback URL. 2. Add the client ID, client secret and callback URL to your environment. 3. Restart the API. Before you start [#before-you-start] You need two URLs from your deployment: | Variable | What it is | Example | | --------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------- | | `DASHBOARD_URL` | The public URL users open in the browser | `https://analytics.example.com` | | `API_URL` | The public URL of the API. With the default Docker Compose / Caddy setup this is the dashboard URL plus `/api` | `https://analytics.example.com/api` | The OAuth callback (the URL the provider redirects back to) always lives on the API: | Provider | Callback URL | Environment variable | | -------- | ---------------------------------- | --------------------- | | Google | `${API_URL}/oauth/google/callback` | `GOOGLE_REDIRECT_URI` | | GitHub | `${API_URL}/oauth/github/callback` | `GITHUB_REDIRECT_URI` | With the example above that is `https://analytics.example.com/api/oauth/google/callback` and `https://analytics.example.com/api/oauth/github/callback`. The callback URL is not derived from `API_URL`. You must set `GOOGLE_REDIRECT_URI` / `GITHUB_REDIRECT_URI` yourself, and the value must be identical to what you register at the provider: same scheme, same host, same `/api` prefix, no trailing slash. If your API is served from its own subdomain such as `https://api.example.com`, drop the `/api` segment. The API and the dashboard must share a domain so the session cookie set by the API callback is readable by the dashboard. `analytics.example.com` + `analytics.example.com/api` works, and so does `app.example.com` + `api.example.com`. `example.com` + `example.org` does not. See [`CUSTOM_COOKIE_DOMAIN`](/docs/self-hosting/environment-variables#custom_cookie_domain) and [`COOKIE_TLDS`](/docs/self-hosting/environment-variables#cookie_tlds) if your domain layout is unusual. Use HTTPS in production; session cookies are marked `secure` whenever `DASHBOARD_URL` uses `https://`. Google [#google] Create or pick a Google Cloud project [#create-or-pick-a-google-cloud-project] Go to the [Google Cloud Console](https://console.cloud.google.com/) and select an existing project or create a new one. Any project works; it does not need billing enabled. Configure the OAuth consent screen [#configure-the-oauth-consent-screen] Open "APIs & Services" and then "OAuth consent screen" (newer consoles call this "Google Auth Platform" and "Branding"). Choose "External" as the user type unless every user is in your Google Workspace organisation. In that case "Internal" is simpler and skips verification entirely. Fill in an app name, support email and developer contact; users see the app name on the consent dialog. The sign-in flow only uses the `openid`, `email` and `profile` scopes. These are non-sensitive and need no verification. An External app starts in Testing mode, which limits sign-in to up to 100 Google accounts that you list as test users. That is fine for a private instance. If you want anyone with a Google account to be able to sign in, click "Publish app". Because only non-sensitive scopes are used, publishing does not require a Google review. If you also plan to enable the [Google Search Console integration](/docs/self-hosting/google-search-console), read the notes there before choosing Testing or Published. The choice affects how long Search Console tokens stay valid. Create OAuth client credentials [#create-oauth-client-credentials] Open "APIs & Services", "Credentials", "Create credentials", "OAuth client ID" and fill in: | Field | Value | | ----------------------------- | -------------------------------------------------------------------------------------------------- | | Application type | Web application | | Name | Anything, e.g. `MakinForU` | | Authorized JavaScript origins | Your `DASHBOARD_URL`, e.g. `https://analytics.example.com` | | Authorized redirect URIs | `${API_URL}/oauth/google/callback`, e.g. `https://analytics.example.com/api/oauth/google/callback` | Click "Create" and copy the client ID (ends with `.apps.googleusercontent.com`) and client secret (starts with `GOCSPX-`). Add the environment variables [#add-the-environment-variables] ```bash title=".env" GOOGLE_CLIENT_ID=123456789012-abcdefghijklmnop.apps.googleusercontent.com GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx GOOGLE_REDIRECT_URI=https://analytics.example.com/api/oauth/google/callback ``` GitHub [#github] Create a GitHub OAuth App [#create-a-github-oauth-app] Go to "Settings", "Developer settings", "OAuth Apps", "New OAuth App" on [github.com](https://github.com/settings/developers). You can create it under your personal account or under an organisation (from the organisation's settings page). Both work the same way. | Field | Value | | -------------------------- | -------------------------------------------------------------------------------------------------- | | Application name | Anything, e.g. `MakinForU` | | Homepage URL | Your `DASHBOARD_URL`, e.g. `https://analytics.example.com` | | Authorization callback URL | `${API_URL}/oauth/github/callback`, e.g. `https://analytics.example.com/api/oauth/github/callback` | Leave "Enable Device Flow" unchecked and click "Register application". Generate a client secret [#generate-a-client-secret] On the app page, copy the client ID, then click "Generate a new client secret" and copy the secret. GitHub only shows it once. Add the environment variables [#add-the-environment-variables-1] ```bash title=".env" GITHUB_CLIENT_ID=Iv1.xxxxxxxxxxxxxxxx GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx GITHUB_REDIRECT_URI=https://analytics.example.com/api/oauth/github/callback ``` MakinForU requests the `user:email` and `user:read` scopes and uses the account's primary, verified email address. Users whose primary GitHub email is unverified will see `GitHub email not found or not verified`. Apply the configuration [#apply-the-configuration] All six variables belong to the API service. The dashboard and worker do not use them; the dashboard asks the API which providers are configured and renders buttons accordingly. Giving every service the full set is harmless, which is what the Docker Compose setup does. How you set them depends on how you deployed MakinForU. With the Docker Compose setup, add them to `self-hosting/.env` (the generated file already contains a commented-out template) and recreate the API container: ```bash docker compose up -d --force-recreate op-api ``` With Coolify, the bundled template exposes them as `MAKINFORU_GOOGLE_CLIENT_ID`, `MAKINFORU_GOOGLE_CLIENT_SECRET`, `MAKINFORU_GOOGLE_REDIRECT_URI`, `MAKINFORU_GITHUB_CLIENT_ID`, `MAKINFORU_GITHUB_CLIENT_SECRET` and `MAKINFORU_GITHUB_REDIRECT_URI`. For any other setup, add them to the API's environment the same way you set `COOKIE_SECRET` and restart the API. Things to know [#things-to-know] Registration rules still apply. A social login that would create a new user is subject to [`ALLOW_REGISTRATION`](/docs/self-hosting/environment-variables#allow_registration) and [`ALLOW_INVITATION`](/docs/self-hosting/environment-variables#allow_invitation). The very first user is always allowed. After that, users need an invite link or `ALLOW_REGISTRATION=true`. Existing users can always sign in. Accounts are not linked automatically. If someone signed up with email/password and later clicks "Sign in with Google" using the same address, they are sent back to the login page with `Please sign in using your original authentication method`. The same applies between Google and GitHub. Users must keep using the method they signed up with. Each provider is independent. Configure one, the other, or both. The login page only shows buttons for providers the API has a client ID and redirect URI for. MakinForU remembers the last provider a browser signed in with (a one-year cookie) and marks that button on the login page with "Used last time". Troubleshooting [#troubleshooting] | Symptom | Likely cause | | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Google shows `Error 400: redirect_uri_mismatch`; GitHub shows "The redirect\_uri MUST match the registered callback URL" | `GOOGLE_REDIRECT_URI` / `GITHUB_REDIRECT_URI` differs from the URL registered at the provider. Check `http` vs `https`, the `/api` prefix and trailing slashes. | | Google shows `Error 401: invalid_client`, or the authorization URL has an empty `client_id` | `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` is not set on the API service, or the API was not restarted after changing `.env`. | | Redirected to `/login?error=Missing oauth parameters` or `OAuth state mismatch` | The state cookie set before the redirect was not sent back to the API. Usually the API and dashboard are on different registrable domains, `DASHBOARD_URL` is wrong, or the browser blocks cookies. See the cookie callout above. | | `/login?error=Registrations are not allowed` | New user and `ALLOW_REGISTRATION=false` with no valid invite. Send an invite from the organisation settings or set `ALLOW_REGISTRATION=true`. | | `/login?error=Please sign in using your original authentication method` | An account with that email already exists via another method. Sign in with that method instead. | | `/login?error=GitHub email not found or not verified` | The user's primary email on GitHub is not verified. Verify it under GitHub settings, "Emails". | | `/login?error=Email not verified with Google` | Google reports the email as unverified. This is rare and usually a Workspace account with a pending domain. | | Google login works for you but others get "Access blocked: This app has not completed the Google verification process" | The consent screen is in Testing and the user is not listed as a test user. Add them, or publish the app. | | Buttons do not appear on the login page | The API does not have both the client ID and the redirect URI for that provider (`GOOGLE_CLIENT_ID` + `GOOGLE_REDIRECT_URI`, or `GITHUB_CLIENT_ID` + `GITHUB_REDIRECT_URI`), or it was not restarted after you added them. | Every error redirect includes a `correlationId` query parameter. Search the API logs for it to see the full error. --- ## Session Replay URL: https://panel.makinforu.com/docs/session-replay Session replay captures a structured recording of what users do in your app or website. You can replay any session to see which elements were clicked, how forms were filled, and where users ran into frictionβ€”without guessing. Session replay is **not enabled by default**. You explicitly opt in per-project. When disabled, the replay script is never downloaded, keeping your analytics bundle lean. How it works [#how-it-works] MakinForU session replay is built on [rrweb](https://www.rrweb.io/), an open-source library for recording and replaying web sessions. It captures DOM mutations, mouse movements, scroll positions, and interactions as structured dataβ€”not video. The replay module is loaded **asynchronously** as a separate script (`op1-replay.js`). This means: * Your main tracking script (`op1.js`) stays lightweight even when replay is disabled * The replay module is only downloaded for sessions that are actually recorded * No impact on page load performance when replay is turned off Limits & retention [#limits--retention] * **Unlimited replays** β€” no cap on the number of sessions recorded * **30-day retention** β€” replays are stored and accessible for 30 days Setup [#setup] Script tag [#script-tag] Add `sessionReplay` to your `init` call. The replay script loads automatically from the same CDN as the main script. ```html title="index.html" ``` NPM package [#npm-package] ```ts title="op.ts" import { MakinForU } from '@makinforu/web'; const op = new MakinForU({ clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, sessionReplay: { enabled: true, }, }); ``` With the npm package, the replay module is a dynamic import code-split by your bundler. It is never included in your main bundle when session replay is disabled. Options [#options] | Option | Type | Default | Description | | -------------------- | --------- | ------------------------------- | ------------------------------------------------------------------------------------ | | `enabled` | `boolean` | `false` | Enable session replay recording | | `maskAllInputs` | `boolean` | `true` | Mask all input field values | | `maskAllText` | `boolean` | `true` | Mask all text content in the recording | | `unmaskTextSelector` | `string` | β€” | CSS selector for elements whose text should NOT be masked when `maskAllText` is true | | `blockSelector` | `string` | `[data-makinforu-replay-block]` | CSS selector for elements to replace with a placeholder | | `blockClass` | `string` | β€” | Class name that blocks elements from being recorded | | `ignoreSelector` | `string` | β€” | CSS selector for elements excluded from interaction tracking | | `flushIntervalMs` | `number` | `10000` | How often (ms) recorded events are sent to the server | | `maxEventsPerChunk` | `number` | `200` | Maximum number of events per payload chunk | | `maxPayloadBytes` | `number` | `1048576` | Maximum payload size in bytes (1 MB) | | `scriptUrl` | `string` | β€” | Custom URL for the replay script (script-tag builds only) | Privacy controls [#privacy-controls] Session replay captures user interactions. All text and inputs are masked by default β€” sensitive content is replaced with `***` before it ever leaves the browser. Text masking (default on) [#text-masking-default-on] All text content is masked by default (`maskAllText: true`). This means visible page text, labels, and content are replaced with `***` in replays, in addition to input fields. This is the safest default for GDPR compliance since replays cannot incidentally capture names, emails, or other personal data visible on the page. Selectively unmasking text [#selectively-unmasking-text] If your pages display non-sensitive content you want visible in replays, use `unmaskTextSelector` to opt specific elements out of masking: ```ts sessionReplay: { enabled: true, unmaskTextSelector: '[data-makinforu-unmask]', } ``` ```html

Product Analytics

Welcome to the dashboard

John Doe Β· john@example.com

``` You can also use any CSS selector to target elements by class, tag, or attribute: ```ts sessionReplay: { enabled: true, unmaskTextSelector: '.replay-safe, nav, footer', } ``` Disabling full text masking [#disabling-full-text-masking] If you want to disable full text masking and return to selector-based masking, set `maskAllText: false`. In this mode only elements with `data-makinforu-replay-mask` are masked: ```ts sessionReplay: { enabled: true, maskAllText: false, } ``` ```html

This will be masked

This will be visible in replays

``` Only disable `maskAllText` if you are confident your pages do not display personal data, or if you are masking all sensitive elements individually. You are responsible for ensuring your use of session replay complies with applicable privacy law. Blocking elements [#blocking-elements] Elements matched by `blockSelector` or `blockClass` are replaced with a same-size grey placeholder in the replay. The element and all its children are never recorded. ```html
This section won't appear in replays at all
``` Or with a custom selector: ```ts sessionReplay: { enabled: true, blockSelector: '.payment-form, .user-avatar', blockClass: 'no-replay', } ``` Ignoring interactions [#ignoring-interactions] Use `ignoreSelector` to exclude specific elements from interaction tracking. The element remains visible in the replay but clicks and input events on it are not recorded. ```ts sessionReplay: { enabled: true, ignoreSelector: '.debug-panel', } ``` Self-hosting [#self-hosting] If you self-host MakinForU, the replay script is served from your instance automatically. You can also override the script URL if you host it separately: ```ts sessionReplay: { enabled: true, scriptUrl: 'https://your-cdn.example.com/op1-replay.js', } ``` Related [#related] * [Session tracking](/features/session-tracking) β€” understand sessions without full replay * [Session replay feature overview](/features/session-replay) β€” what you get with session replay * [Web SDK](/docs/sdks/web) β€” full web SDK reference * [Script tag](/docs/sdks/script) β€” using MakinForU via a script tag