# Bring Your Own Key (BYOK)
Source: https://docs.aikeedo.com/advanced/byok
Learn how to enable and manage workspace-level API keys for OpenAI and Anthropic services in your Aikeedo installation.
## Introduction
BYOK (Bring Your Own Key) allows you to let workspace owners use their own OpenAI and Anthropic API keys. This gives workspaces more control over their AI service usage while reducing your central API costs.
## How It Works
When you enable BYOK:
* Workspace owners can add their own API keys in their workspace settings
* Their workspace will use their custom keys instead of your global API keys
* API costs for that workspace will be charged to their own API keys
* Workspaces without custom keys will continue using your global API keys
BYOK helps distribute API costs across workspaces while maintaining central control over feature availability.
In [session-based usage mode](/billing/overview#usage-mode), BYOK workspaces skip session usage gates for zero-cost OpenAI and Anthropic models only. Other models still consume credits and count toward the session cap.
## Enabling BYOK
1. Go to Settings → Integrations in your admin panel
2. Select either OpenAI or Anthropic
3. Toggle the "Custom API keys (BYOK)" switch to enable
4. Click "Save changes"
You can enable BYOK separately for OpenAI and Anthropic services.
## Administrative Benefits
### Cost Management
* Reduce central API costs by letting workspaces use their own keys
* Maintain global API keys as fallback for workspaces without custom keys
* Better cost distribution across high-usage workspaces
### Control
* Enable/disable BYOK at any time
* Choose which AI services support custom keys
* Keep control over feature availability while delegating API costs
## Important Notes
When disabling BYOK for a service, all workspaces using custom keys will automatically switch to your global API key.
* Workspace owners are responsible for managing their API key validity and costs
* Your global API keys serve as fallback when custom keys are not provided
* Workspace members cannot see the custom API keys set by workspace owners
## Troubleshooting
If workspace owners report issues:
1. Verify BYOK is properly enabled for the relevant service
2. Check if the workspace has entered valid API keys
3. Ensure your global API keys are working as fallback
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Connectors (MCP)
Source: https://docs.aikeedo.com/advanced/connectors
Let the AI in chat use tools from other services — Notion, Linear, Sentry, your own APIs — through remote MCP servers, with each user signing in with their own account.
## Introduction
Connectors let the AI use tools from other services during a chat: search a Notion workspace, look up a Linear issue, read a Sentry error, or call your own internal API. They are built on the [Model Context Protocol](https://modelcontextprotocol.io) (MCP), the open standard that Claude and ChatGPT use for the same purpose, so any remote MCP server works.
Each user connects with **their own account** on the service. The AI acts with that user's access, never someone else's, and the user decides which tools it may use and when it has to ask first.
A user's connectors work in all of their chats, with or without one of your Assistants, on any model that supports tool calling.
There are two kinds of connectors:
* **Catalogue connectors** are ones you add in the admin panel. You choose which plans get them.
* **Custom connectors** are ones a workspace adds itself, by pasting an MCP server's address. Your plans decide whether this is allowed and how many.
Connectors support remote MCP servers over HTTPS, using the Streamable HTTP transport. Servers that run as a local command (stdio) are not supported.
## Before you start
### Encryption key
Connectors store each user's sign-in tokens and request header values **encrypted**, with the key in the `APP_ENCRYPTION_KEY` setting in your `.env` file. Without a key, nobody can connect.
* New installations get a key from the installer.
* Installations updated from a version before 5.1 get one written to `.env` automatically during the update.
If the key could not be written, for example because `.env` is not writable, the **Connectors** page in the admin panel shows a warning. Create the key by running this in your application folder:
```bash theme={null}
php bin/console app:generate-key
```
**Back up `APP_ENCRYPTION_KEY` together with your database.** Stored credentials cannot be read without it. If the key is lost or replaced, every user has to connect every connector again. Never run `app:generate-key --force` on an installation that is already in use.
### Cron
The [cron job](/setup/initial-setup) refreshes each connection's list of tools every few hours. It picks up new and changed tools and keeps sign-ins from expiring for users who have not chatted in a while. Connectors work without cron, but tool lists only change when a user presses **Refresh tools**.
## Turning connectors on
Connectors are off by default. Three switches must all be on for a user to use them:
1. **The feature.** Go to **Settings → Features → Chat** and turn on **Connectors**.
2. **The plan.** Open a plan and turn on **Connectors** under Capabilities. Then choose:
* **Library → Connectors**: which catalogue connectors the plan includes. Check **All connectors** to include every active catalogue connector, including ones you add later.
* **Quota → Custom connectors cap**: how many servers a workspace can add itself. Leave it blank for unlimited, or set it to `0` to disallow custom connectors. Catalogue connectors do not count towards the cap.
3. **The user.** Users can turn connectors off for themselves under **Capabilities**, like any other chat capability.
Existing subscriptions keep the plan settings they were created with. After changing a plan, use **Update snapshots** to apply the change to current subscribers. See [Plans, Snapshots & Subscriptions](/billing/plans-snapshots-subscriptions).
## Adding catalogue connectors
You can add each connector by hand, as described below, or import ready-made ones with the **Connector Catalogue** plugin. It ships 487 vetted MCP servers from official vendors in 14 categories, and requires Aikeedo 5.1 or later.
After installing the plugin, go to **Settings → Starter content → Connectors**, then search, filter by category and import the ones you want. Imported connectors are ordinary connectors: you can edit, switch off or delete them. Connectors that need you to register your own OAuth client are imported switched off, with a note saying so. The catalogue is never part of **Import everything**.
To add a server yourself:
1. Go to **Connectors** in the admin panel and click **Add connector**.
2. Enter the **MCP server URL** and click **Check server**. Aikeedo contacts the server and reports how it signs users in.
3. Fill in the **Details**:
* **Name**, **Description** and **Icon URL** are what users see.
* **Slug** identifies the connector in plan settings. It cannot be changed later.
* **Status**: new connectors can be saved as inactive and turned on when you are ready.
4. Under **Authentication**, choose how users sign in:
* **Users sign in before using it (OAuth)**: the usual choice for services with user accounts.
* **Users sign in only when the server asks (OAuth)**: for servers that allow some tools without signing in.
* **No sign-in**: for public servers, or ones that authenticate with request headers.
5. For OAuth, choose the **OAuth client**:
* **Register with the server automatically** works with most servers.
* **Use a client registered with the server** is for servers that do not register clients automatically. Register an application with the service, using the redirect URI shown on the page, and enter its **Client ID** and **Client secret**.
6. Under **Request headers**, declare any headers the server needs, such as an API key. Give a header a **Shared value** to send the same value for every user. Leave it empty and each user enters their own when they connect. Values are encrypted and never shown again.
7. Under **Tools**, **Tool limit** caps how many of the server's tools are stored.
8. Click **Save changes**, then allow the connector on your plans.
The **Connectors** list also shows the custom connectors workspaces have added. Filter by **Type** to see them. You can open, switch off or delete any of them.
## What users see
Users manage connectors under **Settings → Connectors** in the app.
### Adding a custom connector
When the plan allows it, any workspace member can click **Add custom connector**, enter a name and the server's address, and let Aikeedo check the server. Sign-in and request headers work as in the admin form above, except that header values are always entered by each member.
A custom connector belongs to the workspace, and each member connects to it with their own account. The member who added it and the workspace owner can edit or delete it.
### Connecting
Clicking **Connect** signs the user in to the service, if it needs sign-in, and then lists the server's tools. Tools are grouped by what the server says they do:
* **Read-only tools**
* **Write/delete tools**
* **Other tools**: the server did not say.
* **Unavailable tools**: listed with the reason they cannot be used here, such as an unsupported input definition.
Every tool can be set to **Always allow**, **Needs approval** or **Never allow**, one by one or for a whole group at once. **Every tool starts at Needs approval**, including read-only ones. Groups come from the server's own description of its tools, which Aikeedo cannot verify.
### Approving tools in chat
When the AI wants to use a tool that needs approval, the chat pauses and shows a card with the tool's name and **exactly what it will send**. The user chooses:
* **Allow once**: runs the tool this time.
* **Always allow**: runs it, and changes the tool's permission so it will not ask again.
* **Deny**: the AI is told the request was declined.
Closing the card, or pressing Escape, declines the request and lets the user reply in their own words.
## Credits
Tool calls are not charged. They run against the user's own account with the service. The model's tokens are charged as usual. Every tool offered to the model adds its description to the request, so connecting servers with many tools makes each message larger.
## Security
* **Trust.** A connector receives whatever the AI sends it, and its tools act on the user's account. Users are warned about this when they add a custom connector. Only add catalogue connectors from developers you trust.
* **Untrusted text.** Tool descriptions and results come from a third party. Aikeedo marks them to the model as data, not instructions, and strips hidden characters. This lowers the risk of prompt injection but cannot remove it, which is why tools ask before running by default.
* **Private networks.** Connector addresses must be public HTTPS URLs. Addresses that point to `localhost`, private networks (such as `10.x`, `192.168.x`) or cloud metadata endpoints are refused, including through redirects. The same protection applies to web pages read during a chat and to dataset URLs. As a result, an MCP server running on the same machine or local network as Aikeedo cannot be used as a connector.
* **Credentials.** Tokens and header values are encrypted at rest and never sent back to the browser.
## Troubleshooting
Run `php bin/console app:generate-key` in the application folder, as described in [Encryption key](#encryption-key).
Check that **Connectors** is turned on under **Settings → Features → Chat**.
Check that the connector is active, that their plan includes it under **Library → Connectors**, and that their subscription's snapshot is up to date.
The service requires a pre-registered OAuth application. Register one with the service using the redirect URI shown in the form, and choose **Use a client registered with the server**.
The service no longer accepts the user's sign-in, for example because it was revoked or expired. The user clicks **Sign in** on the connector's page. Their tool permissions are kept.
Tool lists refresh through cron every few hours. Users can press **Refresh tools** on the connector's page to refresh at once.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Email Templates
Source: https://docs.aikeedo.com/advanced/email-templates
Customize and manage multilingual email templates in Aikeedo including welcome emails, password resets, and workspace invitations using Twig templating.
## Introduction
Aikeedo provides customizable email templates for all system communications. This guide covers template location, multilingual support, and customization best practices.
## Template Location
All email templates are stored in the `/resources/emails` directory of your Aikeedo installation. This centralized location makes it easy to manage and update your email content.
## Available Templates
Aikeedo comes with the following essential templates:
### Core Templates
* `layout.twig`: Base layout template that provides a consistent design for all emails
* `export.twig`: Template for admin exported data
* `password-reset.twig`: Sent when a user requests a password reset
* `verify-email.twig`: Used for email verification processes
* `welcome.twig`: Sent to new users upon registration
* `workspace-invitation.twig`: Used when inviting users to join a workspace
All email templates extend layout.twig, which serves as the base layout template providing consistent styling across all emails.
## Multilingual Support
Aikeedo supports multilingual email templates through a directory-based structure.
### Setting Up Multilingual Templates
1. Create a directory in `/resources/emails` that matches your locale string from `locale.json`
```bash theme={null}
# Example for Czech language
/resources/emails/cs-CZ/
```
2. Duplicate the template files into the locale-specific directory
```bash theme={null}
/resources/emails/cs-CZ/welcome.twig
/resources/emails/cs-CZ/verify-email.twig
# ... other templates
```
3. Translate the content in the duplicated templates
The `layout.twig` file in the root directory serves as the default layout for all languages. While you can create language-specific versions of layout.twig, this is optional as the default layout is typically suitable for all languages.
### Template Fallback
* Templates in the root `/resources/emails` directory serve as fallbacks
* If a localized template isn't found, the system will use the corresponding template from the root directory
Even if you only use English, it's highly recommended to:
1. Create an `en-US` directory
2. Duplicate your customized templates there
This prevents your customizations from being overwritten during updates.
## Customizing Templates
You have the flexibility to edit these templates to align with your brand voice and specific needs:
1. Navigate to the appropriate language directory (or create one)
2. Copy the template you want to customize from the root directory
3. Make your changes while keeping the Twig template syntax intact
4. Save the file
After editing any email template, you must clear the app cache for the changes to take effect. This ensures that Aikeedo uses the most up-to-date version of your templates.
## Template Format
Email templates in Aikeedo use the Twig templating engine. If you're new to Twig, you can find more information in the [Twig documentation](https://twig.symfony.com/doc/3.x/).
## Best Practices
When customizing email templates:
* Keep your messaging clear and concise
* Use a consistent tone that matches your brand voice
* Test your emails across different email clients
* Include all necessary information without overwhelming the recipient
* Maintain separate language directories for better organization
* Always backup your customized templates before updates
## Related Guides
* [Mailer Configuration](/email/mailer) - Set up email delivery
* [SMTP Setup](/email/smtp) - Configure SMTP server
* [Localization](/advanced/localization) - Manage language settings
Always clear the application cache after modifying email templates to ensure changes take effect immediately.
# Keyboard Shortcuts
Source: https://docs.aikeedo.com/advanced/keyboard-shortcuts
The keyboard shortcuts available in the app, where they show up, and how users can switch them off.
## Introduction
Aikeedo has keyboard shortcuts for the actions people use most: starting something new, searching, moving around, and managing items. Shortcuts appear where they apply, as small key hints beside buttons and in menus, and a cheat sheet lists all of them.
There is nothing for you to configure. Each user can switch shortcuts off for themselves.
`Ctrl` is shown as `⌘` on Mac. The keys on screen always match the keyboard the user is using.
## Shortcuts
### General
| Shortcut | Action |
| - | - |
| `Ctrl + Shift + O` | **New**: start a new chat, or a new item in the current tool |
| `N` | **Create** on the current page |
| `Ctrl + K` | **Search** |
| `Ctrl + Shift + X` | Turn [Private mode](/advanced/private-mode) on or off |
| `Ctrl + /` | Open the keyboard shortcuts cheat sheet |
### Navigation
| Shortcut | Action |
| - | - |
| `Ctrl + Shift + S` | Show or hide the sidebar |
| `Ctrl + Shift + D` | Show or hide the details panel |
| `Ctrl + Shift + ,` | Open settings |
### Item menu
These work only while a row's **…** menu is open, for example a conversation in Recents. Each key is the first letter of the action.
| Key | Action |
| - | - |
| `U` | Mark as read or unread |
| `S` | Share |
| `R` | Rename |
| `D` | Delete |
| `O` | Open in a new tab |
### Composer
These are listed in the cheat sheet so they are easy to find. They always work and cannot be switched off.
| Key | Action |
| - | - |
| `Enter` | Send message |
| `Shift + Enter` | New line |
| `Esc` | Close a dialog or menu |
## Where users find them
* **On the interface.** Hover over a button, or open an item's menu, to see its shortcut.
* **The cheat sheet.** Press `Ctrl + /`, or choose **Keyboard shortcuts** in the account menu.
* **Settings → Shortcuts.** The full list, grouped, with a switch for each one.
## Switching shortcuts off
Some shortcuts can clash with a browser extension or a screen reader. Users can switch off any of them under **Settings → Shortcuts**, and use **Restore defaults** to go back. The choice is saved to the user's account, so it follows them across browsers.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Localization
Source: https://docs.aikeedo.com/advanced/localization
Translate Aikeedo into any language with the built-in extract, translate and build commands, manage available languages, and let users pick their language.
## Introduction
Aikeedo's interface is translated with GNU gettext. English source text lives in
the code, and translations live in `.po` catalog files you can edit by hand or
fill in automatically.
Three console commands keep everything in step:
| Command | What it does |
| - | - |
| `app:locale:extract` | Scans your code and writes every translatable string into the catalogs |
| `app:locale:translate` | Fills in the empty entries using an AI model |
| `app:locale:build` | Writes the dictionaries the browser loads |
Run them from your installation root:
```bash theme={null}
php bin/console app:locale:extract
```
Extract and translate both run the build step for you, so most of the time you
only need the first two.
## Where translations live
| What | Location |
| - | - |
| App and admin interface | `/locale` |
| Active theme (landing page) | `/extra/extensions/{organization}/{theme}/locale` |
| Plugins | `/extra/extensions/{organization}/{plugin}/locale` |
| Browser dictionaries (generated) | `/{webroot}/locale` |
Where `{organization}` is usually `heyaikeedo`, `{theme}` is `default` for the
standard theme, and `{webroot}` is `public` — or `public_html` on some
installations, such as cPanel.
Inside each of those, catalogs follow the gettext convention:
```
locale/de-DE/LC_MESSAGES/messages.po
```
The `/{webroot}/locale` directory is generated output, not something you edit.
It has to be writable for `app:locale:build` to work.
## Translating
### The usual workflow
After changing any wording in your templates:
```bash theme={null}
php bin/console app:locale:extract
```
You will be asked which targets to scan. Choose **Core application** for the
app and admin interface, **Everything** for that plus every plugin and theme
on your installation, or name them individually — several at once, separated
by commas.
```bash theme={null}
php bin/console app:locale:translate
```
You will be asked which languages to do — **All languages**, or a few by
name — then shown how many strings and API requests that means before
anything is sent.
Open the app in the language you translated. The browser dictionaries are
rebuilt automatically at the end of the previous step.
### Translating by hand
You never have to use the AI translator. Open the `.po` file for your language,
find the `msgid` entries and fill in the `msgstr` fields:
```
msgid "Welcome to Aikeedo"
msgstr "Bienvenue sur Aikeedo"
```
Always leave the `msgid` values unchanged. Only edit `msgstr`.
After editing a `.po` file by hand, rebuild the browser dictionaries:
```bash theme={null}
php bin/console app:locale:build
```
No terminal? **Settings → Clear cache** in the admin area rebuilds them too.
This is the one step that is easy to forget. If your changes show up on
server-rendered pages but not in toasts, buttons or other interface text
produced by JavaScript, the dictionaries are stale — rebuild them.
## The AI translator
`app:locale:translate` sends the untranslated entries to a language model and
writes the answers back into the catalogs.
```bash theme={null}
php bin/console app:locale:translate --lang=de-DE --lang=fr-FR
```
| Option | |
| - | - |
| `--lang`, `-l` | Language to translate; repeatable. Omit to pick from a list |
| `--all`, `-a` | Every language that has a catalog |
| `--force`, `-f` | Retranslate entries that already have a translation |
| `--dry-run` | Show what would be translated and stop |
| `--model`, `-m` | Model to use |
| `--base-url` | An OpenAI-compatible endpoint other than OpenAI's |
| `--batch-size`, `-b` | Strings per request (default 20) |
| `--concurrency`, `-c` | Requests in flight at once (default 4) |
| `--target`, `-t` | Which target to translate; see [Themes and plugins](#themes-and-plugins) |
| `--path`, `-p` | Translate the catalogs of any directory |
The catalog is saved after every group of requests, so stopping the command with
`Ctrl-C` loses at most one group — running it again carries on from where it
stopped.
### Your API key
The translator needs an API key, and it is **your** key for this one task. It has
nothing to do with the AI providers you configured in the admin area, and it is
never saved anywhere.
The key is read from, in order:
1. The `--api-key` option
2. The `AIKEEDO_TRANSLATOR_API_KEY` environment variable
3. The `OPENAI_API_KEY` environment variable
4. A hidden prompt, if none of the above are set
The key is used for the run and then forgotten. It is never written to `.env`,
to your settings, or to the log — so you will be asked for it again next time
unless you set an environment variable.
### Using another provider
Anything that speaks the OpenAI chat completions API works, including
OpenRouter, Groq, Together and a local Ollama server:
```bash theme={null}
php bin/console app:locale:translate \
--base-url=https://openrouter.ai/api/v1 \
--model=
```
### What the translator protects
Strings such as `Hello :name` and `Version %s` contain placeholders that are
replaced at runtime. A translation may move a placeholder to wherever the
target language needs it, but never renames or drops one.
Every answer is checked, and any that gets this wrong is sent back once with
an explanation. If it still comes back wrong, the entry is left untranslated
and listed at the end of the run rather than being written out broken.
Many strings carry a context that says where they appear — `button`,
`heading`, `label`, `country` and so on. This is what stops "Post" the
button and "Post" the noun becoming the same word. The context is sent with
each string so the model can pick the right wording.
Languages need different numbers of plural forms — one for Japanese, two for
German, four for Russian, six for Arabic. The translator asks for exactly the
number your language needs, explains which numbers each form covers, and
rejects an answer with the wrong number of forms.
## Managing available languages
Go to **Settings → Languages** in the admin area. The page lists every language
that ships with Aikeedo, with:
* **Enable toggle** — enabled languages appear in the language switcher and can
be chosen by your users.
* **Default language** — what a visitor sees before they pick one themselves.
Only an enabled language can be the default.
* **Completion** — how much of the app and admin interface is translated for
each language.
Click **Save changes** to apply. The page won't let you disable every language
or make a disabled language the default.
Aikeedo ships configuration for 30 languages. Whether the translations
themselves are filled in depends on the release — run
`app:locale:translate --lang=` to complete any that are not.
### The locale.json catalogue
`/locale/locale.json` is the catalogue of languages the installation knows
about. Your choices on the Languages page are stored in the database and layered
on top of it, so you don't need to edit this file to enable or disable a
language. Its `enabled` flags and `default` are only used as a fallback, for
example before the database exists during installation:
```json theme={null}
{
"default": "en-US",
"locales": [
{
"code": "de-DE",
"label": "German",
"enabled": true,
"dir": "ltr"
}
]
}
```
Updates merge the shipped `locale.json` into yours, so a language added in a
new release shows up on the Languages page of existing installations.
### Adding a language
Add an entry with its code, label and text direction (`ltr` or `rtl`).
```bash theme={null}
php bin/console app:locale:extract
```
```bash theme={null}
php bin/console app:locale:translate --lang=
```
Turn it on in **Settings → Languages**.
## Themes and plugins
All official plugins and themes are translated. Update each installed plugin
and theme to receive its translations.
Themes and plugins keep their own catalogs next to their code. The extract
command lists everything it finds on your installation and asks which to
include:
```bash theme={null}
php bin/console app:locale:extract --target=heyaikeedo/chatbots
php bin/console app:locale:extract --target=all
```
A plugin's catalog is `locale/{language}/LC_MESSAGES/messages.po`. A theme's is
`locale/{language}/LC_MESSAGES/theme.po`, because theme strings belong to their
own `theme` domain.
Installing, updating or removing a plugin rebuilds the browser dictionaries on
its own, so a plugin that ships its own translations works as soon as it is
installed.
What ships inside `extra/` is a built copy of a theme, and building or
updating the theme replaces it — along with any catalog changes made there. If
you are developing a theme from its own repository, point the commands at
that directory instead:
```bash theme={null}
php bin/console app:locale:extract --path=../my-theme
php bin/console app:locale:translate --path=../my-theme --lang=de-DE
```
Pass the repository root. The package inside it — the `static/` directory in
a repository made from the theme starter kit — is found on its own, and its
catalogs are written to `static/locale`.
See the [Theme Development](/development/themes/localization) guide for
how theme packages are laid out.
## Marking text for translation
If you edit templates, wrap any text a person will read so it can be extracted.
The same functions are available in Twig, PHP and JavaScript, and they take the
same arguments everywhere.
| Function | Use it for |
| - | - |
| `__('Save')` | A plain string |
| `p__('button', 'Save')` | A string whose meaning depends on where it appears |
| `n__(':count file', ':count files', n)` | Singular and plural |
| `np__('label', ':count file', ':count files', n)` | Plural with a context |
| `d__('theme', 'Save')` | A string belonging to a theme catalog |
| `dp__('theme', 'button', 'Save')` | A theme string with a context |
| `noop__('Save')` | Marks a string for extraction without translating it here |
The context in `p__()` is never shown to anyone — it only tells the translator
where the string appears.
Plugins use the plain functions (`__()`, `p__()` and so on). Their catalogs are
merged into the application's own dictionary when the plugin is loaded.
Themes name their domain instead — `d__('theme', …)`, `dp__('theme', …)`,
`dn__('theme', …)` or `dnp__('theme', …)`. A plain `__()` in a theme template is
looked up in the application's dictionary, not the theme's, so it is left out
when the theme's strings are extracted.
### Placeholders
Two styles work, and both are passed as the argument after the string:
```twig theme={null}
{{ __('Version %s', version) }}
{{ __('Hello :name', { ':name': user.first_name }) }}
```
```js theme={null}
import { __, n__ } from '../base/translate';
__('Version %s', version);
__('Hello :name', { ':name': user.firstName });
n__(':count credit', ':count credits', count, { ':count': count });
```
### JavaScript
Only plain text can be extracted, so `__(someVariable)` is skipped. That is
deliberate — it is how messages coming back from the server get translated. Such
messages have no text to find anywhere in the JavaScript, so they are listed in
`resources/assets/js/base/server-messages.js` instead.
The browser only receives the strings your JavaScript actually asks for, not the
whole catalog, so pages stay small.
## User language selection
### For signed-in users
Open the account menu at the bottom-left of the app and go to **Settings →
General**.
Under **Preferences**, pick a language from the **Language** selector.
### For website visitors
The default theme puts a language switcher in the website header, usually shown
as a globe icon or the current language code in the top-right corner.
The exact position depends on your theme. Some themes place it in the footer or
a side menu instead.
## Best practices
1. **Consistency** — keep terminology the same across all your translations.
2. **Extract before you translate** — a translation run only fills in what the
catalogs already contain, so extract after changing any wording.
3. **Review machine translations** — the AI translator is a strong starting
point, not a substitute for a native speaker on customer-facing copy.
4. **Test each language** — longer translations can change how a layout fits,
and right-to-left languages need a look of their own.
5. **Keep up with releases** — run extract and translate after each update to
catch newly added strings.
## Troubleshooting
The browser dictionaries are out of date. The quickest fix needs no
terminal: **Settings → Clear cache** in the admin area rebuilds them.
From the command line:
```bash theme={null}
php bin/console app:locale:build
```
If neither changes anything, `/{webroot}/locale` is most likely not
writable — the rebuild is skipped rather than allowed to fail, and the
reason is written to the application log.
Check that it is enabled in **Settings → Languages**, then clear the
application cache.
The answer changed or dropped a placeholder, so it was rejected rather than
written out broken. Run the command again to retry those entries, or fill
them in by hand.
Run `app:locale:extract` first — translation only fills in entries that are
already in the catalogs.
If you still have trouble, check that your `.po` files are correctly formatted,
that `locale.json` is valid JSON, and clear both your browser cache and the
application cache after making changes.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Private Mode
Source: https://docs.aikeedo.com/advanced/private-mode
Let users make everything they create temporary across the whole app. Turn it on once and it stays on until they turn it off.
## Introduction
Private mode is a switch that makes everything a user creates temporary. Chats, images, videos, voice-overs, transcripts, classifications, isolated voices and what chat tools produce are kept out of history, Recents and the library, and are deleted automatically after a retention period you set.
It applies to the whole app, not just chat, and it **stays on** until the user turns it off. Moving between tools, opening settings, reloading the page or opening another tab in the same browser keeps private mode on. Turning it off returns the user to normal mode.
Private mode replaces the temporary chats of v4.1. If you are updating from an earlier version, [turn it on again](#turning-private-mode-on) after the update.
## Turning private mode on
Three things must be true for a user to see the switch.
1. **The feature.** Go to **Settings → Features → Privacy & safety**, turn on **Private mode** and set the **retention period** in days. The default is one day.
2. **The plan.** Open a plan and turn on **Private mode**. The plan editor shows a warning next to the setting while the feature is off globally.
3. **Existing subscribers.** Subscriptions keep the plan settings they were created with. After changing a plan, use **Update snapshots** to apply the change to current subscribers. See [Plans, Snapshots & Subscriptions](/billing/plans-snapshots-subscriptions).
If the feature is turned off or a user's plan no longer includes it, any private mode a browser remembers is switched off automatically.
## What users see
### Switching it on and off
Users switch private mode on with the ghost button beside **New** in the sidebar, or in the header on mobile. The keyboard shortcut is `Ctrl + Shift + X` (`⌘ + Shift + X` on Mac).
While it is on:
* App pages dim so it is clear which mode is active. Settings pages are not dimmed.
* Each tool shows a note under its heading saying that what is created there stays out of history and the library, and is deleted after the retention period.
* If the sidebar is collapsed, a **Private** badge appears in the head bar, which also switches the mode off.
* Lists, including Recents, show only what was created in the current private session. Regular conversations and library items are out of view until the mode is turned off.
### The private session
A private session starts with the first thing the user creates, not when the switch is turned on. Switching the mode on and straight off leaves nothing behind and asks for no confirmation.
Everything created in the session belongs to that browser. It cannot be opened from anywhere else.
### Keeping something
Private items offer **Save** where regular items offer **Share**. Saving an item moves it out of the private session and keeps it.
### Turning it off
If the session holds anything, a prompt tells the user how many items they will no longer be able to open and reminds them to save what they want to keep. The user can cancel or turn the mode off.
If generations are still running, the prompt says so: they finish and count toward usage, but end up out of reach.
## What it does not do
* **It is per browser.** The mode and the session are remembered in that browser. Another browser or device is not in private mode unless the user turns it on there.
* **Items are deleted after the retention period, not immediately.** Until then they exist on your server.
* **It does not change what AI providers keep.** Prompts still go to the model provider, under that provider's data policy.
* **It does not hide usage.** Credits and usage are counted as usual.
## How deletion works
Expired items are removed through the regular delete, so their files on storage and their embeddings go with them. Generations that stayed stuck in a private session are failed and their credit is released. Items that are still generating are left for their provider's webhook to finish.
Deletion runs on the [cron job](/setup/initial-setup), so make sure it is set up.
## Troubleshooting
Check that **Private mode** is on under **Settings → Features → Privacy & safety**, that the user's plan includes it, and that their subscription's snapshot is up to date.
The browser could not keep the setting, or the feature or plan stopped offering it. Private mode is remembered in the browser's storage and in a cookie, so clearing site data turns it off.
Private items are listed only in the private session that made them, and only while private mode is on in that browser. Once the mode is turned off, or the retention period ends, they cannot be opened.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Prompt Templates
Source: https://docs.aikeedo.com/advanced/prompt-templates
Create Writer templates with input tags and attributes, and image and video templates with preview artwork, pinned settings and locking.
Templates give your users a ready-made starting point. Aikeedo has three kinds, all managed from **Templates** in the admin area:
* **Writer templates** turn a prompt with placeholders into a short, step-by-step form. The answers start a chat conversation.
* **Image templates** and **video templates** appear as cards in the Imagine and Video tools, with preview artwork and their own settings.
Aikeedo ships 137 Writer templates, 39 image templates and 24 video templates.
Import them from **Settings → Starter content**. See [Starter
Content](/advanced/seed-data).
## Writer templates
### How users experience them
When a user opens a Writer template, a wizard asks each of the template's inputs on its own screen and lets them pick a model. When they finish, Aikeedo starts a chat conversation. The conversation shows the template's name and the user's answers, and streams the reply like any other chat turn, so tools, Canvas and follow-up questions all work.
Users never see the filled-in prompt. It is sent to the model but is left out
of the page and every API response.
### Recommended model
Set **Recommended model** on a Writer template to prefill the wizard's model picker with a model suited to the template. Users can still change it. If the recommended model is disabled, or the workspace's plan doesn't include it, the wizard falls back to the user's usual model.
### Tags
Tags are the building blocks of Writer templates. They represent input elements that users can customize and are enclosed in curly braces.
```scss theme={null}
{ input_name }
```
### Attributes
Attributes modify the behavior of tags, allowing you to fine-tune how inputs are presented and processed. They are appended to tags using the pipe character (`|`).
```scss theme={null}
{ input_name | attribute1 | attribute2 }
```
### Creating your first Writer template
Let's create a simple prompt template for an "Article Generator":
```scss theme={null}
Write an article about { topic | placeholder: Your chosen subject }.
Use the following draft content:
---
{ content | multiline | placeholder: Draft content }
---
Additional instructions:
- Include relevant statistics and consider diverse perspectives.
- Write it in { language } using a { tone } voice tone.
```
This template asks the user for the topic, draft content, language, and tone, one step at a time.
### Available attributes
Sets a custom label for the input
Example:
`{ input_name | label:Custom label }`
Creates a multiline text input
Example:`{ input_name | multiline }`
Defines the input type (currently supports `enum`)
Example:`{ input_name | type:enum }`
Specifies options for `enum` type inputs
Example:`{ input_name | type:enum | options:Option 1, Option 2, Option 3 }`
Makes an input optional
Example:`{ input_name | optional }`
Sets a custom placeholder text
Example:`{ input_name | placeholder:Custom placeholder }`
Provides additional information about the input
Example:`{ input_name | info:Custom info text }`
### Built-in inputs
Aikeedo provides two convenient built-in inputs to streamline your prompt templates:
1. `language`: A dropdown list of supported languages.
2. `tone`: A dropdown list of supported voice tones.
Use these built-in inputs in your templates like this:
```scss theme={null}
{ language }
{ tone }
```
### Advanced example
Let's create a more sophisticated prompt template for a "Comprehensive Article Generator":
```scss theme={null}
Create a detailed article on { topic | placeholder: Your chosen subject | info: Specify the main focus of your article }.
Target audience: { audience | type:enum | options:General, Beginners, Experts | info: Select your intended readership }
Article structure:
1. Introduction
2. { section_1 | label:Main Section 1 | placeholder:E.g., Historical background }
3. { section_2 | label:Main Section 2 | placeholder:E.g., Current trends }
4. { section_3 | label:Main Section 3 | placeholder:E.g., Future implications }
5. Conclusion
Key points to cover:
{ key_points | multiline | placeholder: List important points to address }
Writing style:
- Language: { language }
- Tone: { tone }
- Length: { word_count | placeholder:Approximate word count | info: Suggested range: 500-2000 words }
Additional requirements:
- Include at least { source_count | type:enum | options:2, 3, 5 | label:Number of sources } credible sources
- Incorporate relevant statistics and data visualizations if applicable
- Address potential counterarguments or alternative viewpoints
Extra notes:
{ extra_notes | multiline | optional | placeholder: Any additional instructions or context }
```
This template combines several attributes and input types into a flexible article brief.
## Image and video templates
Image and video templates appear to users as cards in the Imagine and Video tools. When there are enough of them, a row of templates is also shown above the composer.
To create one, go to **Templates** in the admin area, create a template and set **Type** to **Image template** or **Video template**.
### Card
* **Preview image** — shown on the template card. Use a portrait, 3:4 image.
* **Hover preview** — optional. Plays while the pointer is over the card. GIF, animated WebP, MP4 or WebM.
### Prompt
Write the prompt out in full. Image and video templates don't use `{ }` placeholders.
### Recommended model
Choosing a model prefills it when someone picks the template, and decides which settings can be pinned.
### Pinned settings
Pin any setting the model exposes, such as aspect ratio, resolution or duration. A pinned setting is applied on the server. Anything left on **Let the user choose** stays editable in the composer.
### Input images
Turn on **Require input images** and set a minimum and maximum. The user can't generate until they have attached enough images. Use this for templates that transform a photo, such as a sticker pack or a portrait style.
### Allow extra instructions
Let the user add a note of their own, which is appended to your prompt.
### Locking a template
The **Lock this template** toggle decides how much the user sees:
* **Unlocked** — the template fills the composer as a starting point, and the user can rewrite all of it.
* **Locked** — the user gets the card, the input slots and any setting you left free, and never sees the prompt or the model. The prompt is never sent to the browser and never appears on generations made from the template, including generations shared to the community feed.
A locked template runs on its recommended model and no other, so the
workspace's plan must include that model. Otherwise the card shows as an
upgrade.
## Making templates available on plans
Each plan can limit which templates its workspaces can use. The template list in the plan editor is grouped by type. Locked image and video templates need both the template and its model enabled on the plan.
## Best practices
1. **Be specific**: Clearly define what you want the AI to do in your template.
2. **Use appropriate input types**: Choose the right input type (text, multiline, enum) for each piece of information you need.
3. **Provide context**: Use the `info` attribute to give users helpful information about each input.
4. **Show the result**: Give image and video templates preview artwork that shows what the template produces.
5. **Lock only what you need to**: Lock a template when the prompt is the value you are selling; leave it unlocked when users benefit from editing it.
6. **Test your templates**: Try each template with different inputs and models before making it active.
# Secure URLs
Source: https://docs.aikeedo.com/advanced/secure-urls
Enable secure URL protection in Aikeedo to safeguard uploaded and AI-generated files using tokenized URLs and time-limited access.
## Introduction
Secure URLs protect user-uploaded and AI-generated files through tokenized, time-limited URLs. This guide covers enabling and managing this security feature.
## How It Works
When secure URLs are enabled:
* All files uploaded by your users are protected through tokenized URLs
* The app automatically generates secure URLs whenever files need to be accessed
* Files can only be accessed through valid tokenized URLs
* Direct file URLs without tokens will not work
Secure URLs are completely transparent to your users - they won't notice any difference in how they access files. The app automatically handles all security aspects behind the scenes.
## Managing Secure URLs
### Prerequisites
Before enabling secure URLs, ensure:
* You're using a supported cloud storage provider (like Digital Ocean Spaces)
* Your Aikeedo installation is version 2.10 or higher
* Your Cloud Storage plugin is version 1.2 or higher
Local storage does not support secure URLs. You must configure a cloud storage provider in your admin panel first.
### Enabling the Feature
1. Log in to your Aikeedo admin panel
2. Go to **Settings** > **File storage**
3. Under the Configuration section, ensure your cloud storage adapter is selected
4. Toggle the "Secure URLs" switch to enable the feature
After enabling, all new files uploaded by your users will automatically be protected. Files uploaded before enabling this feature will remain accessible through their original URLs.
## Security Considerations
### URL Access Control
* The app automatically generates new secure URLs whenever files are accessed
* Each secure URL contains a token that expires after 1 hour
* Users won't notice the URL expiration - new secure URLs are generated automatically when needed
* Users cannot bypass the security by sharing direct file URLs
Your users will have continuous access to their files. The 1-hour expiration only applies to individual secure URLs, not to the files themselves. New secure URLs are automatically generated when needed.
If you've enabled CDN in your cloud storage settings (like Digital Ocean Spaces CDN), ensure its edge caching time is set to 1 hour or less. Otherwise, the CDN might continue serving expired secure URLs to your users.
### Best Practices
1. Enable secure URLs before launching your service
2. Avoid disabling secure URLs once enabled
3. Regularly check your storage settings
4. Monitor your users' file access patterns
## Limitations
Important points to consider:
* Local storage cannot use secure URLs
* Some cloud storage providers may not support this feature
* Old URLs will stop working if you disable secure URLs after enabling them
* Once enabled, switching to a different cloud storage provider is not recommended as previously uploaded files will become inaccessible
If you're using Cloudflare R2, secure URLs will only work with the default S3 API endpoint (`.r2.cloudflarestorage.com`) and cannot be used with custom domains.
After enabling secure URLs, stick with your chosen cloud storage provider. Switching providers will break access to files uploaded with secure URLs enabled.
Verify secure URL support with your chosen cloud storage provider before enabling the feature.
## Troubleshooting
If your users report issues accessing files:
1. Check if your cloud storage provider is functioning correctly
2. Verify your CDN settings if enabled
3. Ensure the secure URLs feature is properly enabled
4. Check if the reported files were uploaded after enabling secure URLs
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Starter Content
Source: https://docs.aikeedo.com/advanced/seed-data
Import the AI models, LLM servers, Writer templates and image and video templates that ship with Aikeedo, during installation, from the admin area or from the command line.
Aikeedo ships with a catalog of AI models, ready-made OpenAI-compatible LLM servers, templates and the categories they are filed under. You can bring them in while installing, and import or refresh them at any time afterwards.
## What ships with Aikeedo
| Set | Key | Contents |
| - | - | - |
| LLM servers | `llm-servers` | OpenAI-compatible providers such as Groq, OpenRouter and Together AI, ready for your API key |
| Models | `models` | Models for the built-in integrations, and for the LLM servers and Ollama |
| Categories | `categories` | Groups the Writer templates are filed under |
| Writer templates | `writer-templates` | 137 prompt templates for the Writer tool |
| Image templates | `image-templates` | 39 starting points for Imagine, with preview artwork |
| Video templates | `video-templates` | 24 starting points for Video, with preview artwork |
Sets are always imported in this order. LLM servers come before models because a server's models can only be imported once the server exists, and categories come before Writer templates because templates reference them.
The shipped image and video templates are unlocked and have no recommended
model, so they work with whichever providers you run. Lock them and pin a
model from **Templates** once you know which providers you use. See [Image and
video templates](/advanced/prompt-templates#image-and-video-templates).
## During installation
The installer has a **Starter content** step after the admin account is created. Every set is selected by default, and **Models** is marked as recommended. Clear any you don't want, then click **Finish installation**.
If you are migrating an existing database, only the recommended sets are selected, because that database already has its own templates.
If a set fails to import, the installation still finishes and the success screen lists the sets that didn't import. Import them later from the admin area.
## From the admin area
Go to **Settings → Starter content**. Each set shows how many of its entries are already imported.
* Click **Browse** on a set to see what it contains and import the entries you choose.
* Click **Import everything** to import every set in turn.
The result of each import appears as a notification.
Importing is safe to repeat. Every shipped entry has a fixed ID:
* Templates and categories you already have are updated in place instead of duplicated, and entries you have reordered keep their position and artwork.
* Models and LLM servers you already have are left as they are. Only missing ones are added.
Image and video templates upload their preview artwork to your storage
provider as they are imported, so these sets take a little longer.
### Browsing a set
The **Browse** page lists every entry in the set, with the same search and filters as the other admin lists. Filter by status (**Imported**, **Not imported** or **Not available**) and, for models and Writer templates, by provider or category.
Entries that aren't imported yet are selected when the page opens. Select or clear entries, then click **Import selected**. The status dot on each entry turns green once it is imported.
## LLM servers and their models
The **LLM servers** set contains OpenAI-compatible providers with their server addresses already filled in: OpenRouter, Together AI, Groq, DeepSeek, Google AI Studio, Perplexity, Hugging Face, Nebius, DeepInfra, AI/ML API, Mistral AI, Cerebras and Moonshot AI.
1. Import the servers you want from **Settings → Starter content → LLM servers**. Each server is created without an API key and without models.
2. Import their models from **Settings → Starter content → Models**. A server's models can be selected only after the server is imported. Until then, they are shown faded with a note naming the server they need.
3. Add your API key to each server in **Settings → Integrations → Custom LLM Servers**.
4. Enable the models you want to offer in **Settings → Models**. Imported server models start disabled, so they don't appear to users before the server has a key.
If you already added a server by hand at the same address, it counts as imported. Its models are added to your server and its API key is kept.
Models for [Ollama](/integrations/ollama) work the same way, but they become available once the Ollama integration is set up instead of after an import.
The catalog doesn't include models that the built-in integrations already offer, such as OpenAI, Anthropic, Cohere and xAI models. See [OpenAI API-Compatible Servers](/integrations/openai-api-compatibility) for adding other providers by hand.
## From the command line
```bash theme={null}
php bin/console app:import:seeds
```
Without options, every set is imported. Use `--set` (repeatable) to import specific sets:
```bash theme={null}
php bin/console app:import:seeds --set=image-templates --set=video-templates
```
To add the LLM servers and their models:
```bash theme={null}
php bin/console app:import:seeds --set=llm-servers --set=models
```
`app:import:presets` still works as an alias, so existing deployment scripts keep running.
## Upgrading from v4.x
Before v5.0, the built-in Writer templates were imported with random IDs. If your installation already has them and you import **Writer templates** again, you get a second copy once. Delete the older copies from **Templates** afterwards. New installations are not affected.
# Text Streaming
Source: https://docs.aikeedo.com/advanced/text-streaming
Configure and troubleshoot real-time text streaming for AI responses in Aikeedo including server configuration and hosting compatibility.
## Introduction
Text streaming displays AI responses in real-time as they generate, creating a more engaging user experience. This guide covers functionality, compatibility, and troubleshooting.
## How It Works
Aikeedo's text streaming functionality:
* Works out of the box on most web servers
* Supports shared hosting environments
* Requires minimal configuration in most cases
Text streaming is enabled by default and should work immediately after installation. Most users won't need to make any configuration changes.
## Server Configuration
While Aikeedo works with default server settings in most cases, some servers might need minor adjustments:
### PHP Settings
If streaming isn't working, check these settings in your `php.ini` file:
```ini theme={null}
output_buffering = Off
zlib.output_compression = Off
```
These settings prevent PHP from buffering the output, allowing for immediate transmission of text chunks to the client.
If you're using shared hosting and can't modify php.ini, contact your hosting provider. Many providers can adjust these settings for you if needed.
### Web Server Settings
#### For Nginx Servers
Aikeedo automatically sets the required headers for text streaming. However, if your Nginx is configured as a proxy server, you might need these settings:
```nginx theme={null}
proxy_buffering off;
fastcgi_buffering off;
```
These settings are typically added in the `nginx.conf` file or within the specific server block for your Aikeedo installation.
Most Nginx installations work without any changes, thanks to the `X-Accel-Buffering: no` header Aikeedo sets for streaming responses.
#### For Apache Servers
Apache servers typically work without any changes. If you experience issues, try disabling the gzip module by adding the following to your `/public/.htaccess` file:
```apache theme={null}
RewriteRule ^(.*)$ $1 [NS,E=no-gzip:1,E=dont-vary:1]
```
## Common Issues
### Buffering Issues
If you experience delayed or chunked responses, or responses appear all at once instead of streaming:
1. Verify all buffering-related settings are properly configured
2. Clear your server's cache
3. Restart your web server after making configuration changes
4. Clear your browser cache
5. Try a different browser
6. Check if your hosting provider is using any aggressive caching
### Caching Mechanisms
Various caching layers can affect text streaming:
* Server-level caching
* PHP opcode caching
* Reverse proxy caching
* Browser caching
If responses are delayed, try disabling any caching plugins or features your hosting provider might have enabled.
### Known Hosting Limitations
Some hosting providers may have restrictions that affect text streaming. If you experience issues, contact your hosting provider's support team for assistance.
* **Cloudways**: Text streaming is not supported due to their server stack configuration
* **Shared Hosting**: May have limited configuration options
* **Managed Hosting**: Check with your provider about configuring streaming settings
## Troubleshooting Checklist
Use this checklist to diagnose streaming issues:
* Verify PHP configuration settings
* Check web server buffering settings
* Confirm no interfering caching mechanisms
* Test with different browsers
* Monitor server logs for errors
* Verify hosting provider compatibility
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Users & Workspaces
Source: https://docs.aikeedo.com/advanced/users-workspaces
Understand user types, workspace management, roles, invitations, and multi-tenant architecture in Aikeedo for organizational access control.
Aikeedo's user and workspace system provides flexible access control for individual users and organizations. This guide covers user types, workspace management, and collaboration features.
## User Types
Aikeedo has two main types of users:
1. **Standard Users**: These are the end-users of your website who interact with the AI features and content.
2. **Admin Users**: Administrators have enhanced permissions and can access the admin panel to manage the application.
### Admin Users
Admin users have all the capabilities of standard users, plus additional privileges:
* Access to the admin panel
* Control over application settings
* Ability to manage and analyze user and application data
* Oversight of system-wide data management and analytics
The initial admin user is created during the installation process. This user becomes the app owner.
Admin users can:
* Create new admin accounts
* Manage existing admin accounts (modify permissions, deactivate, or delete)
* Elevate standard user accounts to admin status
By default, it's not possible to sign up directly as an admin user. This is a security measure to protect your application.
## Workspaces
Workspaces are a core concept in Aikeedo, allowing for organized collaboration and resource management.
Key features of workspaces:
* Users can create and own multiple workspaces
* Each workspace can have multiple users
* Users can be part of multiple workspaces
* Every workspace has a single owner
### Workspace Owner Capabilities
Workspace owners have full control over their workspaces, including the ability to:
* Create new workspaces
* Delete existing workspaces
* Invite new users to the workspace
* Remove users from the workspace
* Change the workspace plan
### Default Workspace
When a user signs up for Aikeedo, they are automatically provided with a default workspace. This initial workspace is owned by the new user and serves as their personal starting point within the platform. By default, this workspace is typically named "Personal," reflecting its individual nature. However, to accommodate user preferences and organizational needs, the workspace owner has the flexibility to rename this default space as they see fit. This feature allows users to personalize their Aikeedo experience from the very beginning, tailoring their workspace to align with their specific projects or branding requirements.
## User-Workspace Relationship
The relationship between users and workspaces is many-to-many:
* A single user can create and own multiple workspaces
* A single user can be a member of multiple workspaces (including those owned by others)
* A workspace can have multiple user members
This structure allows for flexible collaboration and resource sharing across teams or projects, while also enabling users to manage multiple distinct workspaces for different purposes.
## Best Practices
To make the most of Aikeedo's user and workspace system:
1. **Carefully assign admin roles**: Only give admin access to trusted individuals who need to manage the application.
2. **Educate users on workspace features**: Help your team understand how to effectively use workspaces for collaboration and organization.
## Related Guides
* [Authentication Settings](/authentication/accounts-settings)
* [Billing Overview](/billing/overview)
* [Initial Setup](/setup/initial-setup)
# Account Settings
Source: https://docs.aikeedo.com/authentication/accounts-settings
Configure user registration, sign-in policies, and email verification requirements in Aikeedo to balance accessibility and security.
Account settings control user registration, authentication, and email verification policies for your Aikeedo platform.
## General Settings
### User Accounts
This setting controls whether users can create and access accounts on your platform.
* When enabled, users can sign in through the sign-in form and API.
* If disabled, only admin accounts will be able to sign in.
Disabling user accounts will prevent regular users from accessing your platform. Use this option with caution.
### Signup Form
This option determines whether new users can register for accounts on your platform.
* When enabled, users can sign up through the registration form and API.
* If disabled, new user registrations will be prevented, but existing users can still sign in.
You might want to disable the signup form temporarily for maintenance or to control user influx.
### Phone Number Collection
Configure whether to collect phone numbers during user registration.
**Available Options:**
* **None**: Phone number collection is disabled for user registration
* **Optional**: Users can optionally provide their phone number
* **Mandatory**: Phone number is required for registration
**Phone Number Collection Purpose:**
When phone number collection is enabled (Optional or Mandatory), you can provide a purpose statement that will be displayed to users during registration.
* **Purpose**: Explain why you need to collect phone numbers
* **Display**: This message will be shown to end users during registration
* **Optional**: Leave blank to hide the explanation
* **Recommended**: Explain the purpose to build trust and compliance
This setting applies to new registrations only and does not affect existing user accounts.
### Disposable Email Domains
Control whether users can register with disposable email addresses.
* **Enabled**: Users can sign up with disposable email domains
* **Disabled**: Registration with disposable email domains is blocked
**Default Protection:**
Aikeedo blocks 2500+ disposable email domains by default, and this list improves with each update to provide better protection against spam registrations.
**Adding Additional Blocked Domains:**
If you need to block additional disposable email domains, you can add them to the `/data/domains.txt` file:
1. Navigate to your Aikeedo installation directory
2. Open or create the `/data/domains.txt` file
3. Add each domain on a new line
4. Save the file
These domains will be added to the default block list.
This setting applies to new registrations only and does not affect existing user accounts.
### Owned Workspace Cap
Limit the number of workspaces a user can own.
* Enter a number to set the maximum limit
* Leave blank or set to "Unlimited" for no limit
* This helps control resource usage and prevents workspace spam
### Account Deletion
Allow users to permanently delete their own accounts.
* **Enabled**: Users see a **Danger zone** section in **Settings → Security** where they can delete their account
* **Disabled**: Only admins can delete user accounts from the admin panel
When a user deletes their account:
* They must confirm the action and enter their password (if they have one)
* Subscriptions for all owned workspaces are cancelled
* All owned workspaces and their data are permanently deleted
* All personal data associated with the account is permanently deleted
Account deletion cannot be undone. Admin accounts cannot delete themselves through self-service.
## Email Verification Policy
The email verification policy determines how strictly your platform enforces email verification for new and existing users. You can choose from three levels of verification:
1. **None**: No email verification is required.
* Users can access all features without verifying their email addresses.
* This option provides the most frictionless user experience but offers the least security.
2. **Relaxed**: Users can sign in, but are encouraged to verify their email.
* Users can access the platform without email verification.
* An alert will be displayed on the dashboard indicating that the email is unverified.
* This setting balances user convenience with a reminder to complete verification.
3. **Strict**: Email verification is mandatory before full platform access.
* Users can sign in but cannot access any pages except account-related pages until they verify their email address.
* This provides the highest level of security but may impact user onboarding.
To enable email verification (for Relaxed and Strict policies), ensure you've configured your [mailer transport](/email/mailer) in the Email settings.
The email verification policy does not apply to admin portal access. Admin accounts are managed separately for security reasons.
### OAuth Email Status
Control how email verification is handled for OAuth (social login) users.
**Available Options:**
* **Ignore**: Email verification status from the identity provider will be ignored
* **Auto**: Email will be marked as verified if verified by the identity provider
* **Verified**: Email will be marked as verified when signing in through an identity provider even if it is not verified by the identity provider
Each option offers different balances between user convenience and security, allowing you to tailor the policy to your platform's needs.
## Best Practices
1. **Regular Review**: Periodically review your account settings to ensure they align with your current security needs and user experience goals.
2. **User Communication**: If you make significant changes to these settings, consider notifying your users to avoid confusion.
3. **Testing**: After changing settings, test the user registration and login processes to ensure everything works as expected.
4. **Compliance**: Ensure your account settings comply with relevant data protection regulations in your jurisdiction.
## Related Guides
* [Identity Providers](/authentication/identity-providers/google) - Set up third-party authentication options
* [CAPTCHA](/authentication/captcha) - Add an extra layer of security to your forms
* [Email Configuration](/email/mailer) - Set up your email system for notifications and verifications
By carefully configuring these account settings, you can create a secure and user-friendly environment on your Aikeedo platform, tailored to your specific needs and user base.
# CAPTCHA Protection
Source: https://docs.aikeedo.com/authentication/captcha
Protect your Aikeedo application from spam and abuse with CAPTCHA integration
CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) is a crucial tool for protecting your Aikeedo application from spam and automated abuse. This guide focuses on setting up and configuring Google reCAPTCHA v2 Invisible, which is currently the supported CAPTCHA solution for Aikeedo.
## Current CAPTCHA Support
At present, Aikeedo supports Google reCAPTCHA v2 Invisible. This powerful and user-friendly CAPTCHA solution offers robust protection without disrupting the user experience.
While only Google reCAPTCHA is currently supported, Aikeedo may introduce additional CAPTCHA options in future updates. Stay tuned to our [changelog](https://aikeedo.com/changelog/?utm_source=docs\&utm_medium=content) for the latest feature additions.
## What is Google reCAPTCHA?
Google reCAPTCHA is a free service that helps protect websites from spam and abuse. reCAPTCHA v2 Invisible uses advanced risk analysis techniques to distinguish between humans and bots without requiring users to solve puzzles or click checkboxes.
## Benefits of Using reCAPTCHA
* **Enhanced Security**: Safeguard your forms and user interactions from spam bots and malicious attacks.
* **Flexible Security Settings**: Adjust protection levels based on your specific requirements and risk tolerance.
## Setting Up Google reCAPTCHA
To integrate Google reCAPTCHA with your Aikeedo application:
1. **Register for reCAPTCHA**:
* Go to the [Google reCAPTCHA Admin Console](https://www.google.com/recaptcha/admin).
* Log in with your Google account.
* Click the "+" button to add a new site.
2. **Configure reCAPTCHA settings**:
* Enter a label for your site (e.g., your domain name) in the "Label" field.
* For "reCAPTCHA type", select "Challenge (v2)".
* Under the Challenge (v2) option, choose "Invisible reCAPTCHA badge".
* In the "Domains" section, add your Aikeedo application's domain(s).
* Under "Google Cloud Platform", you may see a project name if you've used Google Cloud before. If not, a new project will be created for you.
Aikeedo is designed to work with the "Invisible reCAPTCHA badge" option. This provides security without disrupting the user experience.
3. **Complete the registration**:
* Review your settings to ensure everything is correct.
* Click the "Submit" button at the bottom of the page.
4. **Retrieve your Site Key and Secret Key**:
* After submitting, you'll be provided with a Site Key and a Secret Key.
* Keep these keys secure, as you'll need them to configure reCAPTCHA in Aikeedo.
Never share your Secret Key publicly or include it in client-side code. Keep it confidential to maintain the security of your CAPTCHA implementation.
## Configuring CAPTCHA in Aikeedo
Once you have your reCAPTCHA keys, enable and configure it in Aikeedo:
1. Access the Aikeedo admin panel.
2. Navigate to "Settings" > "User accounts" > "CAPTCHA".
3. Toggle on "Protect with Google reCAPTCHA".
4. Input your Site Key in the "Site key" field.
5. Enter your Secret Key in the "Secret key" field.
6. Click "Save changes" to apply the configuration.
Never share your Secret Key publicly or include it in client-side code. Keep it confidential to maintain the security of your CAPTCHA implementation.
## Testing Your CAPTCHA Integration
After setting up CAPTCHA, it's crucial to test its functionality:
1. Open a new incognito/private browsing window or use a different browser.
2. Navigate to your Aikeedo platform's public-facing pages.
3. Attempt actions that should be protected, such as user registration or login.
4. Confirm that the reCAPTCHA challenge activates when appropriate.
It's important to test CAPTCHA integration without logging out of your admin account to avoid potential login issues. Using a separate browsing session ensures you can always access the admin panel if needed.
## Troubleshooting
If you encounter issues with your CAPTCHA integration, consider these steps:
* Verify that your Site Key and Secret Key are correctly entered in the Aikeedo settings.
* Ensure your domain is properly listed in the Google reCAPTCHA admin console.
* Check your browser console for any JavaScript errors related to reCAPTCHA.
* Confirm that your server can communicate with Google's reCAPTCHA API servers.
## Additional Resources
* [Google reCAPTCHA Documentation](https://developers.google.com/recaptcha/docs/invisible)
* [reCAPTCHA FAQ](https://developers.google.com/recaptcha/docs/faq)
# Facebook Identity Provider
Source: https://docs.aikeedo.com/authentication/identity-providers/facebook
Seamlessly integrate Facebook OAuth 2.0 authentication into your Aikeedo application, allowing users to log in with their Facebook accounts. This guide walks you through the setup process, from creating a Facebook App to configuring Aikeedo for a smooth "Login with Facebook" experience.
## Overview
Integrating Facebook as an identity provider offers your users a convenient and secure way to access your Aikeedo application. This feature leverages Facebook's OAuth 2.0 protocol, ensuring a streamlined authentication process while maintaining high security standards.
Before you begin, ensure you have admin access to your Aikeedo application and a Facebook Developer account for setting up the OAuth integration.
## Step-by-Step Integration Guide
### 1. Create a Facebook App
1. Visit the [Facebook for Developers](https://developers.facebook.com/) website.
2. Click on "My Apps" in the top-right corner.
3. Select "Create App" and choose the app type that best fits your needs (e.g., "Consumer" or "Business").
4. Follow the prompts to set up your app, providing the necessary information.
### 2. Configure Facebook Login
1. In your app's dashboard, navigate to the "Add a Product" section.
2. Find "Facebook Login" and click "Set Up".
3. Choose "Web" as the platform for your app.
4. Enter your website's URL in the "Site URL" field.
5. Click "Save" and then "Continue".
### 3. Set Up OAuth Settings
1. In the left sidebar, click on "Facebook Login" > "Settings".
2. In the "Valid OAuth Redirect URIs" field, enter the callback URL provided by Aikeedo.
Look for a click-to-copy feature in Aikeedo for easy access to the correct callback URL.
The callback URL is automatically generated based on your Site Domain and SSL settings. To configure these, visit the [Website Essentials](/website-basics/essentials) page in your Aikeedo admin panel.
3. Scroll down and click "Save Changes".
### 4. Obtain App ID and App Secret
1. In the left sidebar, click on "Settings" > "Basic".
2. Here you'll find your "App ID" and "App Secret". You'll need these for Aikeedo configuration.
Keep your App Secret confidential. Never share it publicly or include it in client-side code.
### 5. Configure Aikeedo
1. Log into your Aikeedo admin panel.
2. Navigate to **Settings > Identity providers > Facebook**.
3. Toggle the switch to enable "Login with Facebook".
4. Enter the "App ID" and "App Secret" obtained from your Facebook App.
5. Click "Save changes" to apply your configuration.
## Testing Your Integration
After completing the setup, it's crucial to test the integration to ensure a smooth user experience:
1. Log out of your Aikeedo application.
2. On the login page, look for the "Login with Facebook" option.
3. Click the option and proceed through the Facebook authentication flow.
4. Verify that you can successfully log in and access your Aikeedo account.
If you encounter any issues during testing, double-check your configuration settings in both the Facebook Developer Console and Aikeedo admin panel.
## Video Tutorial
For a visual guide through this process, check out our step-by-step video tutorial:
## Troubleshooting
If you experience any difficulties with the Facebook identity provider integration, consider the following:
* Ensure the callback URL in Facebook Developer Console matches exactly with the one provided by Aikeedo.
* Verify that you've configured the Facebook Login product correctly in your Facebook App.
* Check that your app's privacy policy and terms of service URLs are set correctly in the Facebook App settings.
## Next Steps
Now that you've successfully integrated Facebook as an identity provider, consider exploring other authentication options to provide your users with more choices:
* [Google Identity Provider](/authentication/identity-providers/google)
* [LinkedIn Identity Provider](/authentication/identity-providers/linkedin)
* [GitHub Identity Provider](/authentication/identity-providers/github)
By offering multiple login options, you can enhance user experience and potentially increase user adoption of your Aikeedo application.
# GitHub Identity Provider
Source: https://docs.aikeedo.com/authentication/identity-providers/github
Seamlessly integrate GitHub OAuth 2.0 authentication into your Aikeedo application, allowing users to log in with their GitHub accounts. This guide walks you through the setup process, from creating a GitHub OAuth App to configuring Aikeedo for a smooth "Login with GitHub" experience.
## Overview
Integrating GitHub as an identity provider offers your users a convenient and secure way to access your Aikeedo application. This feature leverages GitHub's OAuth 2.0 protocol, ensuring a streamlined authentication process while maintaining high security standards.
Before you begin, ensure you have admin access to your Aikeedo application and a GitHub account for setting up the OAuth integration.
## Step-by-Step Integration Guide
### 1. Create a GitHub OAuth App
1. Visit the [GitHub Developer Settings](https://github.com/settings/developers).
2. Click on "OAuth Apps" in the left sidebar, then select "New OAuth App".
3. Fill out the required information:
* Application name: Choose a name for your app
* Homepage URL: Enter your Aikeedo application's URL
* Authorization callback URL: Enter the callback URL provided by Aikeedo
Look for a click-to-copy feature in Aikeedo for easy access to the correct callback URL.
The callback URL is automatically generated based on your Site Domain and SSL settings. To configure these, visit the [Website Essentials](/website-basics/essentials) page in your Aikeedo admin panel.
4. Click "Register application" to create your OAuth App.
### 2. Obtain OAuth Credentials
1. After registering, you'll be taken to your app's settings page.
2. Here, you'll find your "Client ID".
3. Click "Generate a new client secret" to create your Client Secret.
4. Make note of both the Client ID and Client Secret - you'll need these for Aikeedo configuration.
Keep your Client Secret confidential. Never share it publicly or include it in client-side code.
### 3. Configure Aikeedo
1. Log into your Aikeedo admin panel.
2. Navigate to **Settings > Identity providers > GitHub**.
3. Toggle the switch to enable "Login with GitHub".
4. Enter the "Client ID" and "Client Secret" obtained from your GitHub OAuth App.
5. The Valid OAuth Redirect URI is automatically populated based on your [Website Essentials](/website-basics/essentials) settings. Ensure this matches the Authorization callback URL you entered in GitHub.
6. Click "Save changes" to apply your configuration.
## Testing Your Integration
After completing the setup, it's crucial to test the integration to ensure a smooth user experience:
1. Log out of your Aikeedo application.
2. On the login page, look for the "Login with GitHub" option.
3. Click the option and proceed through the GitHub authentication flow.
4. Verify that you can successfully log in and access your Aikeedo account.
If you encounter any issues during testing, double-check your configuration settings in both the GitHub Developer settings and Aikeedo admin panel.
## Video Tutorial
For a visual guide through this process, check out our step-by-step video tutorial:
## Troubleshooting
If you experience any difficulties with the GitHub identity provider integration, consider the following:
* Ensure the Authorization callback URL in GitHub Developer settings matches exactly with the one provided by Aikeedo.
* Verify that you've granted the necessary permissions to your GitHub OAuth App.
* Check that your app's privacy policy and terms of service URLs are set correctly in the GitHub App settings, if required.
## Next Steps
Now that you've successfully integrated GitHub as an identity provider, consider exploring other authentication options to provide your users with more choices:
* [Google Identity Provider](/authentication/identity-providers/google)
* [Facebook Identity Provider](/authentication/identity-providers/facebook)
* [LinkedIn Identity Provider](/authentication/identity-providers/linkedin)
By offering multiple login options, you can enhance user experience and potentially increase user adoption of your Aikeedo application.
# Google Identity Provider
Source: https://docs.aikeedo.com/authentication/identity-providers/google
Seamlessly integrate Google OAuth 2.0 authentication into your Aikeedo application, allowing users to log in with their Google accounts. This guide walks you through the setup process, from creating a Google API project to configuring Aikeedo for a smooth "Login with Google" experience.
## Overview
Integrating Google as an identity provider offers your users a convenient and secure way to access your Aikeedo application. This feature leverages Google's OAuth 2.0 protocol, ensuring a streamlined authentication process while maintaining high security standards.
Before you begin, ensure you have admin access to your Aikeedo application and a Google account for setting up the OAuth integration.
## Step-by-Step Integration Guide
### 1. Create a Google API Project
1. Navigate to the [Google Cloud Console](https://console.cloud.google.com/).
2. Click on the project dropdown at the top of the page.
3. Select "New Project" and provide a name for your project.
4. Click "Create" to generate your new Google API project.
### 2. Set Up the OAuth Consent Screen
1. In the Google Cloud Console sidebar, go to "APIs & Services" > "OAuth consent screen".
2. Choose the appropriate user type for your application (Internal or External).
3. Fill in the required details:
* App name
* User support email
* Developer contact information
4. Click "Save and Continue" to proceed.
### 3. Generate OAuth 2.0 Credentials
1. In the sidebar, navigate to "APIs & Services" > "Credentials".
2. Click "Create Credentials" and select "OAuth client ID".
3. Choose "Web application" as the application type.
4. In the "Authorized redirect URIs" section, enter the callback URL provided by Aikeedo.
Look for a click-to-copy feature in Aikeedo for easy access to the correct callback URL.
The callback URL is automatically generated based on your Site Domain and SSL settings. For more information, visit the [Website Essentials](/website-basics/essentials#site-domain) page.
5. Click "Create" to generate your OAuth 2.0 client ID and secret.
6. Make note of the generated "Client ID" and "Client Secret" - you'll need these for Aikeedo configuration.
### 4. Configure Aikeedo
1. Log into your Aikeedo admin panel.
2. Navigate to **Settings > Identity providers > Google**.
3. Toggle the switch to enable "Login with Google".
4. Enter the "Client ID" and "Client Secret" obtained from your Google API project.
5. Click "Save changes" to apply your configuration.
## Testing Your Integration
After completing the setup, it's crucial to test the integration to ensure a smooth user experience:
1. Log out of your Aikeedo application.
2. On the login page, look for the "Login with Google" option.
3. Click the option and proceed through the Google authentication flow.
4. Verify that you can successfully log in and access your Aikeedo account.
If you encounter any issues during testing, double-check your configuration settings in both the Google Cloud Console and Aikeedo admin panel.
## Video Tutorial
For a visual guide through this process, check out our step-by-step video tutorial:
## Troubleshooting
If you experience any difficulties with the Google identity provider integration, consider the following:
* Ensure the callback URL in Google Cloud Console matches exactly with the one provided by Aikeedo.
* Verify that you've enabled the necessary Google APIs for your project.
* Check that your OAuth consent screen is properly configured and approved (if required).
## Next Steps
Now that you've successfully integrated Google as an identity provider, consider exploring other authentication options to provide your users with more choices:
* [Facebook Identity Provider](/authentication/identity-providers/facebook)
* [LinkedIn Identity Provider](/authentication/identity-providers/linkedin)
* [GitHub Identity Provider](/authentication/identity-providers/github)
By offering multiple login options, you can enhance user experience and potentially increase user adoption of your Aikeedo application.
# LinkedIn Identity Provider
Source: https://docs.aikeedo.com/authentication/identity-providers/linkedin
Seamlessly integrate LinkedIn OAuth 2.0 authentication into your Aikeedo application, allowing users to log in with their LinkedIn accounts. This guide walks you through the setup process, from creating a LinkedIn App to configuring Aikeedo for a smooth "Login with LinkedIn" experience.
## Overview
Integrating LinkedIn as an identity provider offers your users a convenient and secure way to access your Aikeedo application. This feature leverages LinkedIn's OAuth 2.0 protocol, ensuring a streamlined authentication process while maintaining high security standards.
Before you begin, ensure you have admin access to your Aikeedo application and a LinkedIn Developer account for setting up the OAuth integration.
## Step-by-Step Integration Guide
### 1. Create a LinkedIn App
1. Visit the [LinkedIn Developer Portal](https://www.linkedin.com/developers/).
2. Click on "My Apps" in the top-right corner.
3. Select "Create App" and provide the necessary information:
* App name
* LinkedIn Page (company page associated with the app)
* App logo
4. Click "Create app" to proceed.
### 2. Configure OAuth 2.0 Settings
1. In your app's dashboard, navigate to the "Auth" tab.
2. Under "OAuth 2.0 settings", you'll find your Client ID and Client Secret. Make note of these as you'll need them later.
3. In the "Authorized redirect URLs" section, enter the callback URL provided by Aikeedo.
Look for a click-to-copy feature in Aikeedo for easy access to the correct callback URL.
The callback URL is automatically generated based on your Site Domain and SSL settings. To configure these, visit the [Website Essentials](/website-basics/essentials) page in your Aikeedo admin panel.
4. Click "Update" to save your changes.
### 3. Configure App Permissions
1. In the "Products" tab, request access to the necessary LinkedIn APIs (e.g., "Sign In with LinkedIn").
2. Follow the prompts to verify your app and gain access to the required permissions.
### 4. Configure Aikeedo
1. Log into your Aikeedo admin panel.
2. Navigate to **Settings > Identity providers > LinkedIn**.
3. Toggle the switch to enable "Login with LinkedIn".
4. Enter the "Client ID" and "Client Secret" obtained from your LinkedIn App.
5. Click "Save changes" to apply your configuration.
## Testing Your Integration
After completing the setup, it's crucial to test the integration to ensure a smooth user experience:
1. Log out of your Aikeedo application.
2. On the login page, look for the "Login with LinkedIn" option.
3. Click the option and proceed through the LinkedIn authentication flow.
4. Verify that you can successfully log in and access your Aikeedo account.
If you encounter any issues during testing, double-check your configuration settings in both the LinkedIn Developer Console and Aikeedo admin panel.
## Video Tutorial
For a visual guide through this process, check out our step-by-step video tutorial:
## Troubleshooting
If you experience any difficulties with the LinkedIn identity provider integration, consider the following:
* Ensure the callback URL in LinkedIn Developer Console matches exactly with the one provided by Aikeedo.
* Verify that you've requested and been granted access to the necessary LinkedIn APIs.
* Check that your app's privacy policy and terms of service URLs are set correctly in the LinkedIn App settings.
## Next Steps
Now that you've successfully integrated LinkedIn as an identity provider, consider exploring other authentication options to provide your users with more choices:
* [Google Identity Provider](/authentication/identity-providers/google)
* [Facebook Identity Provider](/authentication/identity-providers/facebook)
* [GitHub Identity Provider](/authentication/identity-providers/github)
By offering multiple login options, you can enhance user experience and potentially increase user adoption of your Aikeedo application.
# Billing Overview
Source: https://docs.aikeedo.com/billing/overview
Configure billing settings in Aikeedo including default currency, trial periods, sign-up plans, and fallback subscriptions for user onboarding.
## General Settings
### Billing Address
Control whether billing address information is required during checkout.
* **Enabled**: Billing address will be required at checkout
* **Disabled**: Billing address is optional
Address information is required by payment gateways in some countries for compliance and fraud prevention.
### Credit Overuse Allowance
Manage how overused credits are handled when users exceed their plan limits.
* **Enabled**: Overused credits are tracked as workspace debt and automatically deducted from future subscription renewals or credit purchases
* **Disabled**: Overused credits become lost revenue that cannot be recovered
This feature helps recover revenue from overused credits while still maintaining usage limits. Workspaces with negative balance are still blocked from new operations until debt is covered.
### Charge for Automatic Title Generation
Control whether automatic title generation incurs a cost.
* **Enabled**: Title generation incurs a cost and is charged to the workspace
* **Disabled**: Title generation is provided free of charge
The model used for title generation is set for the whole installation in **Settings → Features → Utilities**.
### Charge for Prompt Enhancement
Control whether the prompt tools in the Imagine and Video composers incur a cost. This covers both **Enhance** and **Surprise me**.
* **Enabled**: Each enhanced or generated prompt incurs a cost and is charged to the workspace
* **Disabled**: The prompt tools are provided free of charge
### Charge for Dictation
Control whether dictation features incur a cost.
* **Enabled**: Dictation incurs a cost and is charged to the workspace
* **Disabled**: Dictation is provided free of charge
### Default Currency
Choose the default currency for your billing system. This setting affects how prices are displayed and transactions are processed throughout your Aikeedo platform.
While it's important to choose a currency supported by your payment gateway, Aikeedo offers flexibility in currency handling. Each payment gateway provides a currency selector with supported currencies. If a selected currency isn't directly supported, Aikeedo can convert it to a supported currency during checkout, provided a currency rate provider is set up.
If the currency option is not set or is set to "Inherit" for payment gateways, the default currency will be used. Ensure you have a currency rate provider configured if you plan to use currencies not directly supported by your payment gateway.
### Currency Rate Provider
Select a provider for currency exchange rates. Options include:
* **None**: No automatic currency conversion.
* **CurrencyAPI**: Uses the CurrencyAPI service for up-to-date exchange rates.
Additional currency providers can be added through plugins, expanding your options for managing multi-currency transactions.
### Detailed Pricing Page URL
Configure where the **Detailed pricing table** button on the in-app Plans page links.
* Use `/#pricing` to point to the built-in landing page pricing section
* Use a full URL (for example, `https://yoursite.com/pricing`) when you use an external marketing site
* Leave empty to hide the button on the Plans page
If you disable the built-in landing page and host pricing on WordPress, Webflow, or another external site, set this URL so the Plans page button does not link to a broken anchor. See [Landing Page Customization](/website-basics/landing-page).
### Credits per Currency
Set how many credits equal one unit of your default currency. This setting is shared across billing modes.
* In **credit metering** mode: converts currency amounts to credits when users purchase add-on credits at checkout
* In **session-based usage** mode: converts add-on credit balances to currency for display in the app
* Enter the number of credits users receive per unit of your default currency
* Example: 100 credits per \$1.00 USD
In session-based usage mode, this field appears under **Usage mode**. In credit metering mode, it appears under **Custom Credit Purchases** when add-on purchases are enabled.
## Trial Configuration
### Trial Period Days
Set the duration of the trial period for new users.
* Enter the number of days for the trial period.
* Set to 0 to disable the trial feature.
Each user is eligible for a trial only once across any paid plan.
### Trial Without Payment
Toggle this option to allow users to start a trial without providing payment information upfront.
* When enabled, users can upgrade to any paid plan during the trial without immediately providing payment details.
* After the trial ends, a payment method will be required to continue the subscription.
## Default & Fallback Plans
### Sign Up Plan
Select the plan to which new users will be automatically subscribed upon registration.
* Choose "None" to disable automatic subscription, requiring users to select a plan themselves.
* Only monthly or yearly recurring plans can be set as the Sign-Up plan.
New users are automatically subscribed to the Sign Up plan, regardless of the trial option. If a trial period is set, users will be downgraded to the Fallback plan after the trial ends. Without a trial, users continue on the Sign Up plan.
If selecting a paid plan as the Sign Up plan, it's crucial to enable the trial period. Otherwise, new users will have immediate access to paid features without payment, which could lead to potential revenue loss.
### Fallback Plan
Define the plan users will be automatically moved to if their current subscription expires or fails to renew.
* Choose "None" to disable automatic downgrading, requiring users to manually select a new plan.
* Only monthly or yearly recurring plans can be set as the Fallback plan.
Ensure you have created appropriate pricing plans before configuring the Sign-Up and Fallback plans to provide a smooth user experience.
## Usage Mode
Choose how usage limits are presented and enforced site-wide at **Settings > Billing > Usage mode**. Credits remain the internal billing unit in both modes.
### Credit Metering (default)
The default mode. Users see credit balances throughout the app.
* Monthly plan credits are the only usage pool
* No session windows or pacing
* Usage logs remain visible
### Session-Based Usage
An alternative that hides credit balances and paces usage through configurable session windows for a smoother, "limitless" feel — this is not unlimited usage.
* Users see session usage as a percentage and when the current session resets
* Credit balances are hidden app-wide; add-on balances display as currency
* A dedicated usage page is available at `/app/billing/usage`
* Usage logs are hidden and redirect to the dashboard
* When the session cap is reached, a limit banner appears in chat with a **Buy usage credits** call to action
#### Session Duration
Set how long each usage session lasts (1–24 hours). Each workspace session starts on first billable usage and resets after the configured duration.
The session cap is calculated from the plan's monthly credits and session duration:
```
floor(monthly_credits × session_hours / (30 × 24))
```
Plans with unlimited monthly credits skip the session cap.
#### How Limits Are Enforced
1. Monthly plan credits are checked first
2. If credits remain, usage is allowed and counts toward the session cap
3. When the session cap is reached, add-on credits allow continued usage
4. If no add-on credits are available, new operations are blocked until the session resets
For add-on credit behavior in session mode, see [Plans, Snapshots & Subscriptions](/billing/plans-snapshots-subscriptions#add-on-credits).
#### BYOK in Session Mode
Workspaces using BYOK (Bring Your Own Key) for OpenAI or Anthropic skip session gates when usage is zero-cost for those providers. Other models still consume credits and count toward the session cap. See [BYOK](/advanced/byok).
## Custom Credit Purchases
### Enable Custom Credit Purchases
Allow users to purchase additional credits beyond their subscription plan.
* **Enabled**: Users can purchase additional credits with custom amounts
* **Disabled**: Users can only use credits included in their subscription plan
### Credits per Currency
When custom credit purchases are enabled in **credit metering** mode, set the exchange rate here. See [Credits per Currency](#credits-per-currency) under General Settings for details.
### Minimum Purchase Amount
Set the minimum amount users must spend when purchasing credits.
* Enter the minimum purchase amount in your default currency
* Example: \$10.00 USD minimum purchase
### Maximum Purchase Amount
Set the maximum amount users can spend in a single credit purchase.
* Enter the maximum purchase amount in your default currency
* Example: \$15,000.00 USD maximum purchase
These limits help control credit purchase volumes and prevent potential abuse while providing flexibility for high-volume users.
## Related Guides
* [Unified Credit System](/billing/unified-credit-system) - Understand credit management
* [Plans, Snapshots & Subscriptions](/billing/plans-snapshots-subscriptions) - Create and manage plans
* [Stripe Integration](/integrations/payment-gateways/stripe) - Configure Stripe payments
* [PayPal Integration](/integrations/payment-gateways/paypal) - Configure PayPal payments
Test your billing configuration thoroughly with test accounts before launching to ensure a smooth user experience.
# Plans, Snapshots, and Subscriptions
Source: https://docs.aikeedo.com/billing/plans-snapshots-subscriptions
Learn how Aikeedo manages subscription plans, plan snapshots for version control, and workspace subscriptions for billing and access control.
## Introduction
Aikeedo's billing system manages user access and resource allocation through plans, snapshots, and subscriptions. This guide explains how these components work together.
## Key Concepts
Before diving into the details, let's familiarize ourselves with some key terms:
* **Workspace**: A collaborative environment where users can access Aikeedo's features and resources.
* **Plan**: A predefined set of features, tools, and credit allocations offered to users.
* **Subscription**: An agreement that grants a workspace access to a specific plan for a set duration.
* **Snapshot**: A frozen copy of a plan's details at the time a subscription is created.
* **Credits**: The currency used within Aikeedo to access various features and resources.
## Workspaces and Subscriptions
Subscriptions are tied to workspaces, not individual users. This allows for flexible team collaboration and resource sharing.
Here's how workspaces and subscriptions interact:
* Each user can own multiple workspaces.
* Each workspace can have its own subscription, independent of other workspaces.
* Multiple members can be added to a workspace, sharing the same set of credits and features.
* Billing details are managed separately for each workspace.
This structure allows for great flexibility in how teams and individuals use Aikeedo, catering to various organizational structures and project needs.
## Plan Types
Aikeedo offers several plan types to suit different user needs:
### Monthly Plans
* Billed automatically every month.
* Provides monthly recurring usage credits.
* Credits reset every 30 days.
### Yearly Plans
* Single annual payment for a year's access.
* Despite yearly billing, credits are still allocated and reset monthly.
* Often comes with a discount compared to monthly billing.
### Lifetime Plans
* One-time payment for indefinite access.
* Monthly recurring credits that reset each month.
* Provides long-term value for committed users.
### Add-on Credits
* Non-recurring, permanent extra credits.
* Can be purchased at any time to supplement an active subscription.
* Used only after regular, recurring credits are depleted.
* Requires an active subscription (free or paid) to be usable.
* Cannot have separate feature configurations.
In the app, add-on credits are labeled **usage credits** — permanent, non-renewing extras used only after the monthly usage limit runs out.
#### Session-Based Usage Mode
When [session-based usage mode](/billing/overview#usage-mode) is enabled:
* Add-on balances are displayed as currency instead of credits
* The session cap still applies while monthly plan credits are being consumed
* When the session cap is reached, add-on credits allow continued usage until the add-on balance is depleted or the session resets
* Users can purchase more add-on credits through the **Buy usage credits** prompt when they hit the session limit
Add-on credits cannot be used without an active subscription, even if they've been purchased.
## Plan Snapshots and Subscriptions
When a user subscribes to a plan, Aikeedo creates a snapshot:
* A snapshot is a read-only copy of the plan at the subscription time.
* It ensures that subscribers retain the features and terms they signed up for, even if the original plan is later modified.
### Updating Plans and Snapshots
* Modifying a plan does not automatically affect existing subscriptions.
* To update current subscriptions (plan snapshots), check the "Update snapshots" checkbox when saving plan changes.
Be cautious when updating snapshots, as it will affect all current subscribers to that plan. Consider communicating changes in advance.
## Plan Customization
Aikeedo allows for granular control over what each plan offers:
* Tools, chat capabilities, models, assistants, and templates (Writer, image and video) can be individually toggled on or off for each plan.
* This allows you to create tiered offerings or specialized plans for different user segments.
## Credit System
Aikeedo uses a unified credit system to manage resource usage:
* Credits are the primary currency for accessing features and resources.
* Regular plans (Monthly, Yearly, Lifetime) provide recurring credits that reset periodically.
* Add-on credits supplement regular credits and don't expire, providing extra flexibility.
For more details on how credits work and are managed, check out our [Unified Credit System](/billing/unified-credit-system) page.
## Best Practices
* Regularly review your plan offerings to ensure they meet your users' needs.
* Communicate clearly about what each plan includes and how credits work.
* Consider offering a mix of plan types to cater to different user preferences and budgets.
* Use add-on credits as a way to upsell without requiring users to change their base subscription.
By understanding and effectively utilizing Aikeedo's plan and subscription system, you can create a flexible, user-friendly billing structure that grows with your business and satisfies your customers' needs.
# Unified Credit System
Source: https://docs.aikeedo.com/billing/unified-credit-system
Understand Aikeedo credit management system for AI operations including credit conversion ratios, usage tracking, and allocation across subscription plans.
## Introduction
The Unified Credit System simplifies usage management by replacing multiple unit types (tokens, image counts, audio seconds) with a single universal unit: credits.
## Key Features
* **Simplified Usage Tracking**: All usage is now measured in credits, making it easier to manage and understand user consumption.
* **Flexible Credit Conversion**: Administrators can define custom credit conversion ratios for different features and AI providers.
* **Flexible Credit Purchases**: Users can buy exact credits needed with better spending control and upfront usage estimates.
* **Improved Plan Management**: Simplifies the process of creating and managing subscription plans with a unified credit allocation.
## How It Works
### Credit Conversion
Credits are used as a universal currency within the Aikeedo platform. Each action (text generation, image creation, audio processing etc.) consumes a specific number of credits based on the defined conversion ratios.
### Setting Conversion Ratios
1. Log in to your Aikeedo admin panel.
2. Navigate to Settings in the left sidebar.
3. Under the Finance section, click on "Credit ratios".
4. Here, you'll find options to set credit costs for various AI models.
5. For each AI model, you can set the credit cost for their specific models or actions.
6. Ensure all fields are filled with appropriate values.
7. The Save button will only become active once all fields have been filled.
8. After filling in all fields and reviewing your changes, click the now-active Save button to apply the new ratios.
The Save button remains inactive until all credit ratio fields are filled. This ensures that no incomplete or potentially erroneous configurations are saved.
## Flexible Credit Purchases
Aikeedo now supports flexible credit purchasing, allowing users to buy exactly the credits they need:
### Upfront Usage Estimates
* **Image Tasks**: See estimated credits before generating images
* **Video Tasks**: Preview credit cost before video generation
* **Text Tasks**: Estimate costs for long-form content
* **Real-time Updates**: Estimates adjust based on selected models and parameters
### Custom Credit Amounts
* **Exact Purchases**: Buy any amount of credits, not just predefined packages
* **Better Control**: Spend only what you need for your projects
* **No Waste**: Avoid unused credits from large packages
* **Instant Access**: Credits are available immediately after purchase
### Negative Balance Handling
* **Overuse Protection**: Overused credits are recorded as negative balance
* **Automatic Deduction**: Negative balance is deducted from your next top-up
* **Transparent Tracking**: Clear visibility of your credit usage and balance
## Credit Calculator
To assist with setting up your credit system, we provide a Credit Calculator tool:
* Available as a Google Sheets document in the Commercial folder of your Aikeedo package
* Free for Extended/Commercial License users
* Helps calculate optimal credit conversion ratios based on your costs and desired pricing
Use the calculator to ensure your credit pricing aligns with your operational costs and profit margins. You can find the Credit Calculator in the [Commercial folder](/overview/commercial-resources) of your Aikeedo package.
## Benefits of the Unified Credit System
1. **Simplified User Experience**: Users deal with a single currency (credits) across all platform features.
2. **Flexible Pricing Models**: Easily create varied plans with different credit allocations.
3. **Easy Resource Management**: Monitor and control resource usage across all features from a single dashboard.
4. **Adaptable to New Features**: As new AI capabilities are added, they can be easily integrated into the existing credit system.
## Best Practices
* Regularly review and adjust your credit conversion ratios to reflect changes in AI service costs.
* Communicate clearly to users how credits are consumed for different actions.
* Use the credit system to create tiered plans that cater to different user needs and usage patterns.
By leveraging the Unified Credit System, you can create more flexible and user-friendly subscription plans while simplifying your platform's usage management. This system provides the foundation for scalable and adaptable pricing strategies as your Aikeedo platform grows and evolves.
# REST API overview
Source: https://docs.aikeedo.com/development/api/overview
Authenticate against the Aikeedo User and Admin REST APIs, choose a workspace, paginate lists, handle errors, and consume streamed chat responses.
Aikeedo includes two JSON REST APIs. Use them to connect your own applications, automations and back-office tools to an Aikeedo installation.
This page applies to Aikeedo 5.x. Class names, paths and signatures match the 5.0 source code.
| API | Base path | Acts as | Typical use |
| - | - | - | - |
| **User API** | `/api` | A regular user inside one of their workspaces | Generate content, manage conversations, library items, presets and workspace data |
| **Admin API** | `/admin/api` | An administrator | Manage users, plans, subscriptions, orders, options and other platform data |
Every installation serves its own interactive reference, generated from the OpenAPI specification that ships with that version:
* User API: `https://your-domain.com/app/api-docs`
* Admin API: `https://your-domain.com/admin/api-docs`
To browse the reference without an installation, see the [User API demo docs](https://demo.aikeedo.com/app/api-docs) and the [Admin API demo docs](https://demo.aikeedo.com/admin/api-docs).
The demo links are for reference only. Always send requests to your own installation.
## Enable the APIs
Both APIs are disabled by default. An administrator enables each one separately.
In the admin panel, go to **Settings → Features → REST API**.
Toggle **User API**, **Admin API**, or both, then select **Save changes**. The change takes effect on the next request.
If an API is disabled, API keys are ignored for that base path and requests fail with `401 Unauthorized`.
## Authentication
Send an API key in the `X-Api-Key` header with every request.
```bash theme={null}
curl https://your-domain.com/api/presets \
-H "X-Api-Key: YOUR_API_KEY"
```
### Get an API key
Each user manages their own key in the app under **Settings → API keys** (`/app/settings/api-keys`). There they can generate a key, regenerate it, or revoke it. A key belongs to a user, not to a workspace.
* **User API:** any active user's key works while the User API is enabled.
* **Admin API:** the key must belong to a user with the `admin` role, and the Admin API must be enabled.
An API key has the same permissions as its owner. Store keys in a secret manager or an environment variable, never in client-side code, and revoke a key right away if it leaks.
### How requests are authenticated
`Presentation\Middlewares\UserMiddleware` resolves the caller for every request, in this order:
1. **`Authorization: Bearer `**: the session token the Aikeedo web app uses.
2. **The `user` session cookie**, used only outside `/api/` and `/admin/api/`.
3. **`X-Api-Key`**: replaces the user above on `/api/*` when the User API is enabled, and on `/admin/api/*` when the Admin API is enabled.
`AuthorizationMiddleware` then rejects requests without an active user. On `/admin` paths it also rejects users who aren't administrators.
If the installation enforces strict email verification, a user with an unverified email address gets `403 Forbidden` on `/api` endpoints until they verify it.
## Select a workspace
User API requests run in the context of a workspace. Credits, plan limits, library items and members all belong to it. By default Aikeedo uses the user's current workspace, which is the one last selected in the app.
To target a different workspace, send its ID in the `X-Workspace-Id` header:
```bash theme={null}
curl https://your-domain.com/api/library/images \
-H "X-Api-Key: YOUR_API_KEY" \
-H "X-Workspace-Id: 0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b"
```
The header applies only if the user is a member of that workspace. If the ID is unknown or the user isn't a member, the request doesn't get a workspace context, and endpoints that need one respond with an error. They don't silently fall back to another workspace.
## Requests and responses
* Send JSON bodies with `Content-Type: application/json`. Endpoints that accept files take `multipart/form-data`.
* IDs are UUID strings.
* Timestamps in responses are Unix timestamps in seconds.
* Money amounts are integers in the currency's minor units; for example, `1999` means 19.99 USD.
### Pagination
List endpoints return a list object:
```json theme={null}
{
"object": "list",
"data": [
{ "id": "0190a1b2-...", "object": "preset" }
]
}
```
Paginate with cursor parameters in the query string:
| Parameter | Description |
| - | - |
| `limit` | Maximum number of items to return. |
| `starting_after` | ID of an item. Returns the items that come after it, for the next page. |
| `ending_before` | ID of an item. Returns the items that come before it, for the previous page. |
| `sort` | Sort field and direction as `field:direction`, for example `created_at:desc`, on endpoints that support sorting. |
To fetch the next page, pass the `id` of the last item on the current page as `starting_after`. An unknown cursor ID returns a `400` validation error whose `param` is the cursor parameter's name. Most list endpoints also have a matching `/count` endpoint that accepts the same filters.
Supported filters and sort fields vary by endpoint. Check each endpoint in your installation's interactive API reference.
## Errors
Aikeedo uses conventional HTTP status codes. `Presentation\Middlewares\ExceptionMiddleware` converts exceptions into JSON error bodies:
| Status | When | Body |
| - | - | - |
| `400 Bad Request` | A parameter failed validation. | `{"code": 0, "message": "...", "param": "field_name"}` |
| `400 Bad Request` | A value object rejected an input value. | `{"message": "..."}` |
| `401 Unauthorized` | The caller isn't authenticated, the key is invalid, or that API is disabled. | `{"message": "...", "param": null}` |
| `403 Forbidden` | Not enough credits, a usage limit was reached, email verification is required, or access to the resource was denied. | `{"message": "..."}` |
| `404 Not Found` | The resource doesn't exist or isn't visible to the caller. | `{"message": "...", "param": null}` |
| `422 Unprocessable Entity` | The request is valid but can't be processed, for example a payment gateway rejected a checkout. | `{"message": "...", "param": null}` |
| `500 Internal Server Error` | An unexpected error occurred. | `{"message": "Internal error", "id": "3f9a1c2b7d4e5a60"}` |
For `500` errors, the `id` is a correlation ID. The same ID appears in the server's error log (`var/log/error-*.log`), so an administrator can find the full stack trace.
With `DEBUG` enabled, unhandled exceptions aren't converted to a `500` response. Never enable debug mode in production.
## Streaming chat responses
Chat generation streams its output as [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events). First create a conversation, then post a message to it and read the response body as a stream.
```bash theme={null}
curl -X POST https://your-domain.com/api/ai/conversations \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'
```
The response contains the conversation `id`.
`POST /api/ai/conversations/{id}/messages` requires `model`, a model key that the workspace's plan allows. It usually includes `content` too. Optional fields include `assistant_id`, `parent_id`, `reasoning`, and uploaded `files[]` when you send `multipart/form-data`.
Each event has an `event` name, a JSON `data` payload and an `id`:
```text theme={null}
event: message
data: {"id":"0190...","object":"message","role":"user", ...}
id: 1726480000.1234
event: text-delta
data: {"type":"text-delta","delta":"Hello","attributes":{}}
id: 1726480000.2345
```
| Event | Meaning |
| - | - |
| `message` | A full message object. Aikeedo sends one for the stored user message and one for the assistant message. |
| `text-delta` | The next chunk of the assistant's answer, in `delta`. |
| `reasoning-delta` / `reasoning-section-start` | Reasoning output from models that support it. |
| `tool-start` / `tool-args` / `tool-call` / `tool-result` | The model calling a tool, such as web search or image generation. |
| `error` | Generation failed. `data` is the error message as plain text, not JSON. For example, the workspace ran out of credits. |
```bash cURL theme={null}
curl -N -X POST https://your-domain.com/api/ai/conversations/CONVERSATION_ID/messages \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o", "content": "Write a haiku about the sea."}'
```
```javascript Node.js theme={null}
import { EventSourceParserStream } from 'eventsource-parser/stream';
const res = await fetch(
`https://your-domain.com/api/ai/conversations/${conversationId}/messages`,
{
method: 'POST',
headers: {
'X-Api-Key': process.env.AIKEEDO_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: 'gpt-4o', content: 'Write a haiku about the sea.' }),
},
);
const events = res.body
.pipeThrough(new TextDecoderStream())
.pipeThrough(new EventSourceParserStream());
for await (const { event, data } of events) {
if (event === 'text-delta') process.stdout.write(JSON.parse(data).delta ?? '');
if (event === 'error') throw new Error(data);
}
```
```python Python theme={null}
import json
import os
import requests
url = f"https://your-domain.com/api/ai/conversations/{conversation_id}/messages"
headers = {"X-Api-Key": os.environ["AIKEEDO_API_KEY"]}
payload = {"model": "gpt-4o", "content": "Write a haiku about the sea."}
with requests.post(url, json=payload, headers=headers, stream=True) as res:
event = None
for line in res.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[len("event: "):]
elif line.startswith("data: ") and event == "text-delta":
print(json.loads(line[len("data: "):]).get("delta", ""), end="", flush=True)
```
Reverse proxies can buffer streamed responses. Aikeedo sends `X-Accel-Buffering: no` for nginx. If your proxy or CDN buffers the response anyway, turn off response buffering for `/api/ai/conversations/*/messages`.
## Related
JWTs, cookies, API keys, roles and access control inside the core app.
How API routes, middleware stacks and error mapping are wired.
Add your own workspace-scoped endpoints under `/api` from a plugin.
Stream Server-Sent Events from your own plugin endpoints.
# Coding standards and quality tools
Source: https://docs.aikeedo.com/development/coding-standards
The conventions Aikeedo code follows, and the static analysis, style and test tooling that ships with the application.
Follow these conventions in plugins, themes and any core customization. They match the existing code, so your work stays readable to anyone who knows the codebase, and it passes the bundled quality tools.
## PHP conventions
* **PSR-12** formatting and **PSR-4** autoloading.
* Every file starts with `declare(strict_types=1);` after the opening tag.
* Type every property, parameter and return value. Use union and nullable types instead of untyped values.
* Add `#[Override]` when you implement or override a method, and import `Override`.
* Use constructor property promotion for dependencies.
* Keep classes final-ish in spirit: prefer composition and small services over deep inheritance.
```php src/Services/RateFetcher.php theme={null}
Command` | `CreateCategoryCommand` |
| Command handler | `Handler` | `CreateCategoryCommandHandler` |
| Domain event | `Event` | `CategoryCreatedEvent` |
| Request handler | `RequestHandler`, or `*View` for pages | `CreateCategoryRequestHandler` |
| Value object | A plain noun | `Title`, `Price`, `CreditCount` |
### Domain modeling
Core code keeps domain rules inside the domain layer, and you should do the same in a plugin:
* **Value objects** validate their input in the constructor and throw `Shared\Domain\Exceptions\InvalidValueException` when it's wrong. They're immutable.
* **Entities** expose behavior, not setters for every field. They take value objects, not raw scalars.
* **Commands** are simple data holders that convert scalars into value objects, and they're tagged with `#[Handler(...)]`.
* **Handlers** do the work, persist through a repository, and dispatch a domain event.
```php src/Application/Commands/CreateNoteCommand.php theme={null}
title = new Title($title);
}
}
```
See [Command bus](/development/core/command-bus) and [Modules and layers](/development/core/modules-and-layers) for the full pattern.
## Twig conventions
* Use `{% extends "/layouts/main.twig" %}` for app and admin pages, and put your markup in the blocks that layout defines.
* Register your own namespace and reference templates as `@acme-hello/...`.
* Guard optional variables with `is defined`, because debug mode enables strict variables.
* Wrap every user-facing string in a translation function: `__()`, `p__()` with a context, `n__()` for plurals. Themes use the `theme` domain, so they call `d__('theme', ...)` and `dp__('theme', ...)`.
## JavaScript conventions
* Alpine.js drives interactivity. Register components with `Alpine.data('name', () => ({ ... }))`.
* Call the JSON API through the `window.api` client so authentication headers are handled for you.
* Reuse the built-in web components, such as `x-form` and `modal-element`, instead of reimplementing them.
## Quality tools
The installation ships with its own tooling and Composer scripts:
| Command | Tool | What it checks |
| - | - | - |
| `composer phpstan` | PHPStan, level 5 | Static analysis of `src`, excluding `src/Presentation` |
| `composer phpcs` | PHP\_CodeSniffer | PSR-1, PSR-12 and PHP 8.2 compatibility |
| `composer phpcbf` (or `composer fix`) | PHP\_CodeSniffer | Fixes the style issues it can fix automatically |
| `composer phpmd` | PHPMD | Common code smells, configured by `phpmd.xml` |
| `composer unit-test` | PHPUnit | The suite in `tests/` |
| `composer code-coverage` | PHPUnit | An HTML coverage report in `coverage/` |
| `composer analyse` | All of the above | Runs PHPStan, PHP\_CodeSniffer, PHPMD and PHPUnit in order |
Configuration lives in `phpstan.neon.dist`, `phpcs.xml.dist`, `phpmd.xml` and `phpunit.xml`. Point the tools at your own package to check it:
```bash theme={null}
vendor/bin/phpcs --standard=phpcs.xml.dist extra/extensions/acme/hello/src
vendor/bin/phpstan analyse --level=5 extra/extensions/acme/hello/src
```
Give your package its own `composer.json` scripts and CI so it can be checked outside an Aikeedo installation too.
## Testing your extension
The bundled PHPUnit configuration loads `bootstrap/autoload.php` and runs the suite in `tests/`. For a plugin, test the parts you can isolate: value objects, services, mappers and API clients, using a PSR-18 client double instead of live HTTP calls. Verify integration points, such as routes, settings pages and webhooks, against a local installation with `DEBUG=true`.
## Documentation and versioning
* Ship a `README.md` with installation and configuration steps.
* Use semantic versioning in your package's `version` field, and raise it with every release, because the installer passes it to Composer.
* Record which Aikeedo versions you support.
* Translate your strings and ship the catalogs. See [Plugin localization](/development/plugins/localization) and [Theme localization](/development/themes/localization).
## Related
* [Modules and layers](/development/core/modules-and-layers)
* [Testing and quality](/development/core/testing-and-quality)
* [Plugin quickstart](/development/plugins/quickstart)
# Get your add-on listed
Source: https://docs.aikeedo.com/development/community-add-ons
We're looking for community-built Aikeedo plugins and themes to list on aikeedo.com. Here's what to send us.
We're looking for well-built plugins and themes from the community to list alongside the official add-ons on [aikeedo.com](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content), so the people running Aikeedo can find them.
## How a community listing works
**Listing is free.** We don't charge for a listing, and we don't take a cut of your sales.
**It stays yours.** You keep the code, set your own price, and distribute it from your own site.
**The listing points to you.** It describes your add-on and links to your page, where people buy or download it.
**You support it.** Questions, updates and compatibility with new Aikeedo releases are yours to handle.
## What to send
Email [sales@aikeedo.com](mailto:sales@aikeedo.com) with:
* **What it is** — the name, what it does, and who it's for, in a few sentences
* **Where it lives** — the page where people can get it, plus documentation if you have it
* **What it looks like** — screenshots or a short video, or a link to a live demo
* **Compatibility** — the Aikeedo versions you've tested it against
* **Support** — where customers reach you, and who maintains it
## Before you get in touch
Install it on a clean Aikeedo installation and go through it once: install, configure, use, update, uninstall. The packaging pages cover what a solid release looks like.
Packaging, versioning and the release checklist for a plugin.
Building, packaging and publishing a theme.
## What happens next
We look at every add-on ourselves and reply either way. If we list it, we'll confirm the details with you first.
# AI subsystem
Source: https://docs.aikeedo.com/development/core/ai-subsystem
How Aikeedo resolves AI services for a model, streams generations, runs tools, and turns provider usage into credits.
Everything AI-related goes through one indirection: ask the factory for a capability and a model, and get back a service that supports it. Providers are interchangeable, and a plugin can add more.
## Resolving a service
```php theme={null}
use Ai\Domain\Completion\MessageServiceInterface;
use Ai\Domain\Services\AiServiceFactoryInterface;
$service = $this->factory->create(MessageServiceInterface::class, $model);
```
The factory walks its registered services, resolves each from the container, and returns the first that implements the requested interface and reports `supportsModel($model)`. If nothing matches, it throws.
Every service implements `Ai\Domain\Services\AiServiceInterface`:
```php theme={null}
public function supportsModel(Model $model): bool;
public function getSupportedModels(): Traversable;
```
## Capabilities
| Interface | Method |
| - | - |
| `Completion\MessageServiceInterface` | `generateMessage()`, returns a `Generator` of stream parts |
| `Completion\TextCompletionServiceInterface` | `complete()`, a single instruction and input |
| `Image\ImageServiceInterface` | `generateImage()` |
| `Video\VideoServiceInterface` | `generateVideo()`, often asynchronous |
| `Speech\SpeechServiceInterface` | `getVoiceList()`, `generateSpeech()` |
| `Speech\VoiceCloningServiceInterface` | `cloneVoice()`, `deleteVoice()` |
| `Transcription\TranscriptionServiceInterface` | `generateTranscription()` |
| `Classification\ClassificationServiceInterface` | `generateClassification()` |
| `Embedding\EmbeddingServiceInterface` | `generateEmbedding()` |
| `IsolatedVoice\VoiceIsolatorServiceInterface` | `generateIsolatedVoice()` |
## Bundled providers
Registered during boot: OpenAI, Anthropic, Cohere, xAI, Google, Azure, ElevenLabs, Speechify, Stability AI, Clipdrop, Fal.ai, Luma, Ollama, and a generic adapter for OpenAI-compatible servers. Each implements the capabilities its API supports.
Video generation is asynchronous at several providers, which is why the application exposes provider-specific webhook endpoints that complete a generation when the provider calls back.
## The model registry
What users can select comes from the registry, not from the services:
| File | Role |
| - | - |
| `config/registry/base.json` | The catalog shipped with the release |
| `config/registry/registry.json` | Per-installation customizations, not shipped, so updates preserve it |
| `config/registry/import.json` | Importable models for bundled providers |
| `config/registry/servers.json` | Importable OpenAI-compatible servers |
| `config/registry/schema.json` | The JSON schema for the above |
At boot, `base.json` is loaded and `registry.json` is merged over it, matching services and models by key.
Services and models that don't exist in `base.json` survive the merge only when marked `"custom": true`. That flag is what lets an administrator's custom server, or a plugin's models, persist.
A model entry carries its key, type, name, a cost multiplier, modalities, capability specs, the rate keys it bills under, and UI configuration such as prompt length and accepted images.
## Chat generation
```mermaid theme={null}
sequenceDiagram
participant C as Client
participant A as MessageApi
participant H as GenerateMessageCommandHandler
participant S as Provider service
participant B as BillingService
C->>A: POST /api/ai/conversations/{id}/messages
A->>H: GenerateMessageCommand
H->>B: assertCanConsume
H->>S: generateMessage(model, message)
S-->>H: stream parts
H-->>A: yields parts
A-->>C: server-sent events
H->>B: consume(cost)
```
The handler returns a generator, and the request handler streams it. Parts are also folded into the stored message, so reloading the conversation shows the same content.
### Stream parts
| Part | Meaning |
| - | - |
| `TextDeltaPart` | The next chunk of the answer |
| `ReasoningDeltaPart`, `ReasoningSectionStartPart` | Reasoning output |
| `ToolStartPart`, `ToolArgsPart`, `ToolCallPart`, `ToolResultPart` | A tool being called and returning |
| `SourcePart` | A cited source |
| `LibraryItemPart` | Something generated and saved, such as an image |
| `UsagePart` | The running cost total |
| `ErrorPart` | Generation failed |
| `FinishPart` | The stream is done |
Only some of these are forwarded to the browser; the rest are internal. See the [REST API overview](/development/api/overview) for the client-visible events.
## Tools
Tools are what a model can call mid-answer: web search, page fetching, YouTube lookups, media generation, transcription, knowledge base and embedding search, memories and chat history, canvas documents, follow-up questions and voice selection.
They're held in a collection, and filtered per message: the tool must be enabled, allowed by the workspace's plan, and not disabled by the user. Temporary conversations exclude the tools that would leave state behind.
A tool returns its textual result plus its cost, which is added to the generation's total. See [AI tools](/development/plugins/guides/ai-tool).
## Embeddings and knowledge bases
Uploaded documents and links are chunked, embedded, and stored in a vector store. The default store keeps vectors as files in the configured storage; an alternative can be selected from the registered stores. Search is scoped by namespace and by the dataset units attached to the conversation. See [Vector stores](/development/plugins/guides/vector-store).
Documents are parsed by a reader stack that handles PDF, Word, HTML, XML, JSON, YAML, CSV and plain text.
## Credits
`Ai\Infrastructure\Services\CostCalculator` converts provider usage into credits using the installation's configured rates, which are keyed per model and per rate type such as input, output or image. The lifecycle is:
`estimate($model)` returns the model's multiplier as a pre-flight figure.
`BillingService::reserve()` holds the estimate so concurrent work can't overspend.
The provider call runs.
The reservation is released, and the real cost is consumed, which dispatches a credit usage event.
When a workspace brings its own provider key, the billing service skips deduction, which is why the model is always passed alongside the workspace.
## Related
* [Billing subsystem](/development/core/billing-subsystem)
* [AI providers guide](/development/plugins/guides/ai-provider)
* [AI models and credits](/development/plugins/ai-models-and-credits)
* [Unified credit system](/billing/unified-credit-system)
# Authentication and authorization
Source: https://docs.aikeedo.com/development/core/authentication-and-authorization
How Aikeedo identifies users with tokens, cookies and API keys, resolves workspaces, and decides what each caller may do.
Authentication happens in middleware, before a request handler runs. Authorization is split: coarse checks in middleware, fine-grained checks in access control classes.
## Identifying the caller
`UserMiddleware` tries three sources, in order:
The session token the web app holds. It's verified, and the user it names is loaded.
A cookie named `user`, holding the same kind of token. It's used **only outside** `/api/` and `/admin/api/`, so browser credentials can't authenticate an API call.
An API key replaces the user above, on `/api/*` when the User API is enabled, and on `/admin/api/*` when the Admin API is enabled.
If none match, the request continues unauthenticated, and a later middleware decides whether that's allowed.
## Session tokens
| Property | Value |
| - | - |
| Algorithm | HS256, signed with the `JWT_TOKEN` secret |
| Claims | `sub`, `iat`, `jti`, `exp`, plus `is_admin`, `email`, `first_name`, `last_name` and `avatar` |
| Default lifetime | 24 hours |
| Remembered lifetime | 30 days |
Signing in returns the token in the response body and sets the cookie. The cookie is `httpOnly`, `SameSite=lax`, and marked secure when the site is configured for HTTPS. `COOKIE_PREFIX` and `COOKIE_DOMAIN` adjust its name and scope for installations that share a domain with other applications.
Changing `JWT_TOKEN` invalidates every session and API token immediately. Treat it as a secret with no rotation story: rotate it only when you intend to sign everyone out.
## API keys
Each user has one key, managed under **Settings → API keys** in the app. It carries the owner's permissions, which means a key belonging to an administrator can call the Admin API when that API is enabled.
The two APIs are switched on separately under **Settings → Features → REST API**. When an API is off, its key authentication is skipped entirely, and requests fail as unauthenticated.
## Workspaces
Almost everything belongs to a workspace rather than a user. After identifying the caller, the middleware resolves one:
1. The `X-Workspace-Id` header, when present.
2. Otherwise the user's current workspace.
The workspace is attached to the request **only if the user is a member**, so a handler that reads it can trust it:
```php theme={null}
$user = $request->getAttribute(UserEntity::class);
$workspace = $request->getAttribute(WorkspaceEntity::class);
```
## Authorization
| Layer | Enforces |
| - | - |
| `AuthorizationMiddleware` | An active user must exist; under `/admin`, the user must have the admin role |
| `EmailVerificationMiddleware` | A verified email, when the policy is strict |
| Access control classes | Per-resource rules |
Roles are deliberately coarse: `user` and `admin`. Anything finer is a per-resource decision.
### Access control classes
`Presentation\AccessControls\*` holds the resource rules, with a `Permission` enum covering the actions the application distinguishes, such as reading, editing and deleting, plus resource-specific ones.
Two conventions are worth copying into your own code:
* **A resource the caller may not see is a `404`**, not a `403`, so IDs can't be probed.
* **A resource that exists but is withheld by the caller's plan is a `403`**, because telling them it exists is the point.
```php theme={null}
if ($preset->getStatus() !== Status::ACTIVE) {
throw new NotFoundException(param: 'preset');
}
if (!self::grants($preset, $workspace)) {
throw new HttpException(statusCode: StatusCode::FORBIDDEN);
}
```
## Signing in
| Endpoint | Purpose |
| - | - |
| `POST /api/auth/basic` | Email and password sign-in |
| `POST /api/auth/signup` | Registration |
| `POST /api/auth/recovery/...` | Password recovery |
| `GET /auth/{provider}` | Single sign-on callback |
Single sign-on ships with Google, GitHub, Facebook and LinkedIn providers, each configured in the admin panel. Captcha validation can be required on the public forms.
## Bring your own keys
`ByokMiddleware` applies a workspace's own provider credentials when the installation allows it. The billing service then skips credit deduction for calls made with those keys, which is why AI services always receive the model alongside the workspace.
## Extending
* To protect your own endpoints, extend the base class that already carries the middleware you need. See [Routes and request handlers](/development/plugins/routes-and-request-handlers).
* To authenticate someone who isn't an Aikeedo user, such as a widget visitor, issue your own scoped token and verify it in your own middleware. See [Public pages and endpoints](/development/plugins/public-endpoints-and-embeds).
## Related
* [Routing and middleware](/development/core/routing-and-middleware)
* [REST API overview](/development/api/overview)
* [Users and workspaces](/advanced/users-workspaces)
# Background jobs and cron
Source: https://docs.aikeedo.com/development/core/background-jobs-and-cron
How Aikeedo runs scheduled and deferred work without a queue: the cron event, batched listeners and provider webhooks.
Aikeedo has no queue worker. Work that can't finish inside a request happens in one of three places: the cron tick, a provider webhook, or a batched job driven by cron.
## The cron tick
`cron.php` boots the application, dispatches a single event, and flushes:
```php theme={null}
$dispatcher->dispatch(new CronEvent());
```
Scheduling it is part of installing Aikeedo:
```bash theme={null}
* * * * * /usr/bin/php /path/to/aikeedo/cron.php
```
Run it as the same user as the web server. A cron job running as root creates cache and upload files the web server can't overwrite, which shows up later as a cache that won't clear.
See [Initial setup](/setup/initial-setup) for the production configuration.
## What runs on each tick
| Listener | Work |
| - | - |
| Renew subscriptions | Resets usage for subscriptions that are due |
| End cancelled subscriptions | Ends expired ones and applies the fallback plan |
| Calculate MRR | Updates revenue statistics |
| End failed generations | Closes generations that never completed |
| Prune cache | Removes expired cache entries |
| Purge expired library items | Deletes items past their retention |
| Process import jobs | Advances conversation imports |
| Save last run | Records the timestamp, at low priority so it runs last |
Plugins subscribe to the same event. See [Events and cron](/development/plugins/events-and-cron).
## Batching and cursors
A tick may arrive every minute, so listeners process a bounded batch and remember where they stopped. Renewal, for example, handles a limited number of workspaces per run and stores its position in an option such as `option.cron.renew_subscriptions.cursor_id`.
That pattern is worth copying:
Process a fixed number of records per run.
Persist a cursor, so the next run continues.
Keep a status, so an administrator can start, pause and stop the work.
Return early when there's nothing to do, so an idle installation costs nothing.
## Import jobs
Imports are the closest thing to a queue: an upload creates a job record, and each tick advances it through the adapter in batches, tracking progress and skipping duplicates. The job carries its own status and counters, so the interface can show progress. See [Import adapters](/development/plugins/guides/import-adapter).
## Provider webhooks
Some work isn't scheduled at all: it finishes when a third party says so. Video and image generation at several providers is asynchronous, and the provider calls a dedicated endpoint when the result is ready. Payment providers do the same for cancellations and asynchronous settlements.
| Endpoint | Purpose |
| - | - |
| `POST /webhooks/{gateway}` | Payment gateway events |
| Provider-specific media endpoints | Completed generations |
See [Payment webhooks](/development/plugins/guides/payments/webhooks).
## Timing and reliability
* **Entity changes are flushed** after the cron event finishes, the same as in a web request.
* **There's no retry.** A listener that throws stops that listener; the next tick starts fresh. Catch your own exceptions and record failures.
* **Ticks can overlap** if one run takes longer than the interval. Keep batches small enough to finish well inside a minute.
* **Nothing is guaranteed to run on time.** Some hosts limit cron to every 15 minutes, which delays renewals accordingly.
## Checking it works
```bash theme={null}
php cron.php
tail -n 50 var/log/app-$(date +%F).log
```
The admin panel's status page shows the last recorded run, which is the quickest way to tell whether the schedule is firing at all.
## Related
* [Events](/development/core/events)
* [Plugin events and cron](/development/plugins/events-and-cron)
* [Billing subsystem](/development/core/billing-subsystem)
* [Initial setup](/setup/initial-setup)
# Billing subsystem
Source: https://docs.aikeedo.com/development/core/billing-subsystem
Plans, snapshots, orders, subscriptions and credits in Aikeedo, and how payments, tax and currency plug into them.
Billing ties together what a customer bought, what their workspace may do, and how much of it they've used.
## The model
```mermaid theme={null}
flowchart LR
P[Plan] -->|frozen copy| S[Plan snapshot]
S --> O[Order]
O -->|fulfilled| SUB[Subscription]
SUB --> W[Workspace]
W -->|consumes| C[Credits]
```
| Entity | Role |
| - | - |
| `PlanEntity` | What's for sale: price, cycle, credits, capabilities, features |
| `PlanSnapshotEntity` | A frozen copy taken when someone buys, so later plan edits don't rewrite history |
| `OrderEntity` | One purchase: currency, totals, tax, coupon, gateway, status |
| `SubscriptionEntity` | Ongoing access: usage, renewal dates, the provider's reference |
| `CouponEntity` | A discount, optionally limited to a number of cycles |
## Enumerations
| Enum | Values |
| - | - |
| `BillingCycle` | `one-time`, `monthly`, `yearly`, `lifetime` |
| `OrderStatus` | `draft`, `pending`, `failed`, `processing`, `completed`, `cancelled` |
| `SubscriptionStatus` | `active`, `trialing`, `canceled`, `ended`, `unknown` |
`BillingCycle` also answers two questions the rest of the system asks: whether the cycle recurs, and whether it renews.
## What a plan grants
`PlanConfig` is the capability side of a plan: per-feature settings for chat, writer, imagine, video, transcription, voice-over, voice isolation and classification, plus allowed models, tool switches, allowed assistants and templates, and an `extensions` map plugins can add to.
Because a snapshot carries the config, a subscription keeps the capabilities it was sold with. See [Plan config extensions](/development/plugins/guides/plan-config-extension).
## Credits
Credits live on the workspace, in three places:
| Field | Meaning |
| - | - |
| Subscription credits | Granted by the plan, reset each period |
| Add-on credits | Bought separately, or granted by a one-time plan |
| Session credits | Used in session billing mode |
A `null` credit count means unlimited. Deduction takes from the subscription first, then the add-on balance.
`BillingService` is the gate everything goes through:
| Method | Purpose |
| - | - |
| `assertCanConsume()` | Throws when the workspace can't spend right now |
| `reserve()` | Holds an estimate for the duration of a call |
| `release()` | Returns a held estimate |
| `consume()` | Charges the real cost and dispatches a credit usage event |
Relevant options: `option.billing.usage_mode`, which is either unpaced or session-based, and `option.billing.negative_balance_enabled`.
## Purchase flow
Creating an order snapshots the plan, applies a coupon, and asks the configured tax engine for tax lines.
The selected gateway starts the payment, and the user is redirected or shown an embedded form.
On return, the gateway verifies the payment and hands back a provider reference. The order is paid.
For a recurring plan, a subscription is created from the order, carrying the reference and the gateway. For a one-time plan, credits are added to the workspace. Any previous subscription is cancelled.
The full sequence, including webhooks, is in [Payment gateways](/development/plugins/guides/payments/overview).
## Renewals
A cron listener renews subscriptions that are due, in batches, tracking its position in an option. Renewal resets usage, settles any credit debt, and moves the next reset 30 days out, then dispatches a usage reset event.
Renewal resets usage; it doesn't charge anyone. Either the provider bills on its own schedule, or a gateway plugin charges when the usage reset event fires.
Cancelling sets the subscription to end at its next renewal date and asks the gateway to cancel at the provider. A second cron listener ends expired subscriptions, moving the workspace to the plan configured as `option.billing.fallback_plan`.
## Payment gateways
Gateways are registered in a factory, keyed by a lookup key, and implement a small interface for purchase, completion, cancellation and webhooks. Marker interfaces describe how the gateway is presented and whether it supports a given plan. Aikeedo ships Stripe, PayPal, bank transfer and manual payment.
## Tax
A tax engine calculates lines when an order is created, and the result is stored on the order, so gateways charge the taxed total without extra work. The selected engine comes from `option.billing.tax_engine`, falling back to a no-op engine. See [Tax engines](/development/plugins/guides/tax-engine).
## Currency
Plans are priced in the platform currency. When a gateway bills in another one, a helper converts through the configured rate provider, and silently falls back to the original amount if conversion fails. See [Currency rate providers](/development/plugins/guides/currency-rate-provider).
## Commands
| Command | Purpose |
| - | - |
| `CreateOrderCommand` | Start a purchase, including snapshot, coupon and tax |
| `PayOrderCommand` | Record payment with a gateway reference |
| `FulfillOrderCommand` | Grant the plan or credits |
| `CancelSubscriptionCommand` | End at period end, and tell the gateway |
| `EndSubscriptionCommand` | End now, and apply the fallback plan |
| `RenewSubscriptionCommand` | Reset usage for a due subscription |
| `ReadSubscriptionCommand::createByExternalId()` | Find a subscription from a webhook |
## Related
* [Payment gateways](/development/plugins/guides/payments/overview)
* [AI subsystem](/development/core/ai-subsystem)
* [Plans, snapshots and subscriptions](/billing/plans-snapshots-subscriptions)
* [Unified credit system](/billing/unified-credit-system)
# Bootstrap and request lifecycle
Source: https://docs.aikeedo.com/development/core/bootstrap-and-lifecycle
How an Aikeedo request starts: container, providers, bootstrappers, plugins, routing, and the final flush.
Every entry point, web, console or cron, runs the same bootstrap. Knowing its order explains why plugins can rely on core services, and why database changes are saved after the response is sent.
## Entry points
| Entry point | Used by |
| - | - |
| `public/index.php` | Every HTTP request |
| `bin/console` | Console commands |
| `cron.php` | Scheduled work |
All three include `bootstrap/app.php` and get back a configured container.
## Boot sequence
```mermaid theme={null}
flowchart TD
A[Entry point] --> B[bootstrap/autoload.php]
B --> C[bootstrap/container.php]
C --> D[new Application container, debug]
D --> E[Service providers register]
E --> F[Bootstrappers run in order]
F --> G[Plugins boot]
G --> H[Request handled]
```
`bootstrap/autoload.php` loads Composer's autoloader and maps the application's namespaces to `src/`. Application classes have no vendor prefix, so `Billing\Domain\Entities\OrderEntity` lives at `src/Billing/Domain/Entities/OrderEntity.php`.
`bootstrap/container.php` loads `.env`, falling back to `.env.example`, builds the configuration object, registers the configuration resolver, and loads the provider, bootstrapper, command and migration lists.
`new Application($container, DEBUG)` configures error reporting, promotes warnings to exceptions, sets the timezone to UTC, and stores a static container so `Application::make()` works anywhere.
Each provider registers low-level services: logging, caching, events, HTTP factories, the HTTP client, the service collection, the emitter and router, and the Twig environment.
Module bootstrappers run in a fixed order. They bind repository interfaces to Doctrine implementations, register navigation, wire event listeners, and populate the registries: AI services, tools, payment gateways, storage adapters and more.
The plugin bootstrapper runs near the end, so every core registry exists before a plugin's `boot()` is called. The active theme's template namespace is registered here too.
For HTTP, the request is built from globals, dispatched through the router and middleware pipeline, and the response is emitted.
After the response is emitted, the entity manager is flushed once, which is when database changes are written.
## Providers
Providers run first, in this order:
| Provider | Registers |
| - | - |
| `LoggerServiceProvider` | Monolog handlers writing to `var/log` |
| `CacheServiceProvider` | Filesystem PSR-6 and PSR-16 caches in `var/cache` |
| `EventServiceProvider` | The PSR-14 dispatcher and its listener mappers |
| `HttpFactoryServiceProvider` | PSR-17 request, response and stream factories |
| `HttpClientServiceProvider` | The PSR-18 HTTP client |
| `ServiceCollectionProvider` | The keyed service registry |
| `HttpServiceProvider` | The response emitter and the router dispatcher |
| `ViewEngineProvider` | Twig, its loader paths and extensions |
They're listed in `config/providers.php`.
## Bootstrappers
`config/bootstrappers.php` fixes the order:
1. `FileSystemBootstrapper` and `DoctrineBootstrapper`, so storage and the database exist first.
2. `OptionModuleBootstrapper`, which makes settings resolvable as `option.*` container IDs.
3. The domain modules: user, workspace, dataset, category, preset, billing, voice, AI, import, statistics, assistant, affiliate, file, chatbot.
4. `ConsoleBootstrapper`, `RoutingBootstrapper`, `MailerBootstrapper`.
5. `PluginModuleBootstrapper`, which loads plugins and the active theme.
6. `PreferencesBootstrapper`, which resolves the chosen vector store and rate provider from what's now registered.
This order is why a plugin can register a payment gateway or an AI service in `boot()`: the registries it adds to were created by bootstrappers that already ran.
## Configuration resolution
Two resolvers make configuration injectable by ID:
| Prefix | Source |
| - | - |
| `config.*` | Paths and flags computed at bootstrap, such as `config.dirs.root` and `config.enable_caching` |
| `option.*` | Rows in the options table, decoded from JSON into a dot map |
If the database isn't configured yet, option resolution returns nothing rather than failing, which is what lets the installer run.
## Request handling
```mermaid theme={null}
sequenceDiagram
participant I as index.php
participant R as Router
participant M as Middleware pipeline
participant H as Request handler
participant E as Emitter
I->>R: dispatch(request)
R-->>M: matched handler + middleware
M->>H: handle(request)
H-->>M: response
M-->>I: response
I->>E: emit(response)
I->>I: EntityManager::flush()
```
Route parameters become request attributes, and middleware is assembled from the handler's class hierarchy, outermost first. See [Routing and middleware](/development/core/routing-and-middleware).
## Persistence timing
Handlers call repository methods and entity setters; nothing is written until the flush at the end. That has three consequences worth remembering:
* A handler can build and modify entities freely without worrying about partial writes.
* A response is emitted **before** the flush, so a failure there can't be reported to the client.
* Long-running work, such as a streamed response, must flush explicitly if it needs data persisted mid-request.
Console commands and the cron entry point flush as well.
## Theme preview
Before the application boots, `bootstrap/app.php` looks for a preview cookie. When it names an installed theme, the active theme is overridden for that request only, which is how `/preview?theme=...` works without changing the live site.
## Related
* [Dependency injection](/development/core/dependency-injection)
* [Configuration](/development/core/configuration)
* [Routing and middleware](/development/core/routing-and-middleware)
* [Plugin lifecycle](/development/plugins/lifecycle-and-hooks)
# Caching and logging
Source: https://docs.aikeedo.com/development/core/caching-and-logging
What Aikeedo caches, how to clear it, where logs are written, and how unhandled errors are recorded.
## Caching
Caching is governed by two flags. Everything below is on only when `CACHE=true` **and** `DEBUG=false`.
| What | Where |
| - | - |
| Compiled Twig templates | `var/cache` |
| Route discovery | `var/cache` |
| Event listener discovery | `var/cache` |
| Doctrine metadata and proxies | `var/cache` |
| Application data, through PSR-6 and PSR-16 | `var/cache` |
Debug mode disables all of it, which is why development sees template and route changes immediately.
### The cache pools
A filesystem-backed PSR-6 pool is available to any service, with a PSR-16 wrapper for simpler uses:
```php theme={null}
use Psr\Cache\CacheItemPoolInterface;
$item = $this->cache->getItem('acme.rates');
if (!$item->isHit()) {
$item->set($this->fetchRates());
$item->expiresAfter(3600);
$this->cache->save($item);
}
return $item->get();
```
The cache is disposable. Clearing it, or a deployment, removes everything, so never keep state there that has to survive.
### Clearing
`Shared\Infrastructure\CacheManager::clearCache()` clears both pools, empties the cache directory, and rebuilds the browser translation catalogs. It's exposed in the admin panel under **Status → Clear cache**, and it runs automatically when a plugin is installed, activated, deactivated or uninstalled.
Clear the cache after:
* Adding or changing a route, in an installation with caching on
* Changing a template on a cached installation
* Editing translation catalogs
* Any manual change inside `extra/extensions/`
If clearing reports success but nothing changes, check ownership of `var/cache`. A cron job running as a different user than the web server is the usual cause.
## Logging
Two rotating handlers write to `var/log`:
| File | Level | Retention |
| - | - | - |
| `app-YYYY-MM-DD.log` | Debug and above | `LOG_MAX_FILES`, 14 days by default |
| `error-YYYY-MM-DD.log` | Error and above | `LOG_ERROR_MAX_FILES`, 30 days by default |
```php theme={null}
use Psr\Log\LoggerInterface;
public function __construct(
private LoggerInterface $logger,
) {}
$this->logger->info('Sync started', ['workspace' => (string) $workspace->getId()]);
$this->logger->error('Provider call failed', ['exception' => $th]);
```
Pass context as an array rather than interpolating it into the message, so entries stay searchable.
## Unhandled errors
When an exception reaches the exception middleware and isn't one it maps, it generates a correlation ID, logs the exception with the request path and method, and returns:
```json theme={null}
{ "message": "Internal error", "id": "3f9a1c2b7d4e5a60" }
```
Search that ID in `var/log/error-*.log` to find the trace. Non-API requests get an empty `500` instead, with the same log entry.
With `DEBUG=true` the exception is rethrown rather than logged and swallowed, so you see it directly.
## Plugin failures
A plugin that fatals while booting is recorded in `var/plugin-health.json` and skipped afterwards, so one broken plugin can't take the installation down. See [Debugging plugins](/development/plugins/debugging).
## What to watch in production
| Signal | Meaning |
| - | - |
| Growth in `error-*.log` | Something is failing repeatedly; the correlation IDs group them |
| `var/plugin-health.json` exists | A plugin crashed and is being skipped |
| Cache clears that don't take effect | File ownership problem |
| `var/` growing without bound | Log retention, or temporary files that aren't cleaned up |
## Related
* [Configuration](/development/core/configuration)
* [Routing and middleware](/development/core/routing-and-middleware)
* [Debugging plugins](/development/plugins/debugging)
# Command bus
Source: https://docs.aikeedo.com/development/core/command-bus
How Aikeedo dispatches commands and queries to their handlers, and how to define your own.
Behavior in Aikeedo is expressed as a command and a handler. A request handler builds a command, dispatches it, and turns the result into a response. The same mechanism serves reads and writes.
## Dispatching
```php theme={null}
use Shared\Infrastructure\CommandBus\Dispatcher;
public function __construct(
private Dispatcher $dispatcher,
) {}
$category = $this->dispatcher->dispatch(new CreateCategoryCommand('Marketing'));
```
`Dispatcher::dispatch(object $cmd): mixed` reads the `#[Handler]` attribute from the command's class, walking up to parent classes if needed, resolves that handler from the container, and calls `handle()`. A command with no handler throws `Shared\Infrastructure\CommandBus\Exception\NoHandlerFoundException`.
There is no queue and no middleware: the handler runs synchronously, in the current request.
## A command
```php src/Category/Application/Commands/CreateCategoryCommand.php theme={null}
title = new Title($title);
}
}
```
The command accepts scalars, because callers work with raw input, and converts them into value objects, so validation happens once and early.
## A handler
```php src/Category/Application/CommandHandlers/CreateCategoryCommandHandler.php theme={null}
class CreateCategoryCommandHandler
{
public function __construct(
private CategoryRepositoryInterface $repo,
private EventDispatcherInterface $ed,
) {}
public function handle(CreateCategoryCommand $cmd): CategoryEntity
{
$category = new CategoryEntity(title: $cmd->title);
$this->repo->add($category);
$event = new CategoryCreatedEvent($category);
$this->ed->dispatch($event);
return $category;
}
}
```
Handlers are resolved from the container, so their dependencies are autowired. They persist through a repository and let the end-of-request flush write the change.
## Commands and queries
Both use the bus. The naming convention makes the intent clear:
| Prefix | Returns |
| - | - |
| `Create*`, `Update*`, `Delete*` | The affected entity, or nothing |
| `Read*` | One entity, or throws a not-found exception |
| `List*` | An iterator of entities |
| `Count*` | An integer |
```php theme={null}
$plan = $this->dispatcher->dispatch(new ReadPlanCommand($id));
$cmd = new ListPlansCommand();
$cmd->setStatus(1);
$cmd->setOrderBy('price', 'ASC');
foreach ($this->dispatcher->dispatch($cmd) as $plan) {
// …
}
```
List commands usually expose setters for filters, sorting, limits and cursors, rather than a long constructor.
## Named constructors
Some commands offer alternative constructors for lookups by something other than an ID:
```php theme={null}
$cmd = ReadSubscriptionCommand::createByExternalId('demo-pay', $providerId);
$subscription = $this->dispatcher->dispatch($cmd);
```
## Generators
A handler may return a `Generator`, which is how streamed AI responses work: the handler yields parts as the provider produces them, and the request handler streams them to the client. See [Streaming responses](/development/plugins/streaming-responses).
## Errors
Handlers throw domain exceptions rather than returning error values:
```php theme={null}
try {
$cmd = new PayOrderCommand($id, $gateway, $reference);
$this->dispatcher->dispatch($cmd);
} catch (AlreadyPaidException) {
// Someone else completed it first.
}
```
Presentation-layer exceptions are turned into HTTP responses by the exception middleware. See [Routing and middleware](/development/core/routing-and-middleware).
## Using the bus from a plugin
Plugins dispatch core commands, and define their own:
```php theme={null}
#[Handler(SendGreetingCommandHandler::class)]
class SendGreetingCommand
{
public function __construct(
public readonly string $email,
) {}
}
```
Both sides are autowired, so a plugin's handler can depend on core services and on its own. See [Dependency injection](/development/plugins/dependency-injection-and-services).
## Conventions
One command, one handler, one responsibility.
Commands hold data and convert it into value objects; they contain no logic.
Handlers dispatch a domain event after a change, so other code can react.
Handlers return entities or iterators, not formatted output. Formatting belongs in a resource.
Nothing is flushed inside a handler unless it genuinely must be.
## Related
* [Modules and layers](/development/core/modules-and-layers)
* [Events](/development/core/events)
* [Dependency injection](/development/core/dependency-injection)
# Configuration
Source: https://docs.aikeedo.com/development/core/configuration
Aikeedo's three layers of configuration: environment variables, computed config values, and database-backed options.
Configuration comes from three places, with different lifetimes and audiences.
| Layer | Set by | Where it lives | Read as |
| - | - | - | - |
| Environment | The person installing | `.env` | `env('NAME')` |
| Config | Computed at bootstrap | Memory | `config.*` container IDs |
| Options | Administrators in the panel | The `option` table | `option.*` container IDs |
## Environment variables
`.env` is loaded at bootstrap, falling back to `.env.example` for keys it doesn't define. These settings belong to the installation, not the business.
### Application
| Variable | Purpose |
| - | - |
| `ENVIRONMENT` | `prod`, `dev`, `demo` or `install`. `install` routes everything to the installer; `demo` is used for public demo installations. |
| `DEBUG` | Shows errors, disables caches, makes Twig strict, and rethrows plugin boot failures. |
| `CACHE` | Enables template, route, listener and metadata caching, when debug is off. |
| `JWT_TOKEN` | Secret used to sign session tokens. |
| `APP_ENCRYPTION_KEY` | Master key for credentials encrypted at rest, such as connector sign-ins. `base64:` followed by 32 random bytes; generated by the installer, by the 5.1 update, or by `php bin/console app:generate-key`. Back it up with the database: replacing or losing it makes stored credentials unreadable. |
| `PUBLIC_DIR` | Web root directory name, such as `public` or `public_html`. |
### Database
| Variable | Purpose |
| - | - |
| `DB_DRIVER` | `mysql`. Leaving it empty means no database, which is how the installer boots. |
| `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` | Connection details. |
| `DB_CHARSET` | Character set, normally `utf8mb4`. |
| `DB_UNIX_SOCKET` | Socket path, as an alternative to host and port. |
### Frontend and assets
| Variable | Purpose |
| - | - |
| `HMR` | Load application assets from the Vite dev server instead of the build. |
| `ASSETS_SERVER` | That dev server's URL, defaulting to `http://localhost:5173`. |
| `THEME_ASSETS_SERVER` | The theme dev server's URL, normally port 5174. Overrides theme asset resolution whenever it's set. |
### Sessions, logs and tooling
| Variable | Purpose |
| - | - |
| `COOKIE_DOMAIN`, `COOKIE_PREFIX` | Cookie scope and naming, for multi-app domains. Misconfiguring these breaks sign-in. |
| `LOG_MAX_FILES`, `LOG_ERROR_MAX_FILES` | How many rotated log files to keep. |
| `COMPOSER_HOME` | Composer's home directory, used when installing plugins. |
Read them with the global helper, which takes a default:
```php theme={null}
$environment = env('ENVIRONMENT', 'prod');
```
Environment variables are exposed to Twig as the `env` global. Read a specific key when a template genuinely needs one, and never dump or serialize the object.
## Config values
Computed at bootstrap and injected by ID:
```php theme={null}
use Easy\Container\Attributes\Inject;
public function __construct(
#[Inject('config.dirs.uploads')]
private string $uploads,
#[Inject('config.enable_debugging')]
private bool $debug = false,
) {}
```
| ID | Value |
| - | - |
| `config.dirs.root` | Installation root |
| `config.dirs.webroot` | Web root, honoring `PUBLIC_DIR` |
| `config.dirs.cache`, `.log` | `var/cache`, `var/log` |
| `config.dirs.src`, `.views` | `src`, `resources/views` |
| `config.dirs.uploads` | `{webroot}/uploads` |
| `config.dirs.locale` | `locale` |
| `config.dirs.extensions` | `extra/extensions` |
| `config.dirs.artifacts` | `extra/artifacts` |
| `config.enable_debugging` | `DEBUG`, as a boolean |
| `config.enable_caching` | `CACHE`, as a boolean |
| `config.locale` | The language catalogue from `locale/locale.json` |
Two more IDs are set alongside them: `version`, read from the `VERSION` file, and `license`.
## Options
Options are rows of JSON, edited by administrators. They're loaded once per request and exposed as dot paths:
```php theme={null}
#[Inject('option.billing.currency')]
private ?string $currency = 'USD',
#[Inject('option.features.chat.is_enabled')]
private ?bool $chatEnabled = false,
```
In Twig they're all under `option`:
```twig theme={null}
{{ option.site.name ?? '' }}
{% if option.features.chat.is_enabled|default(false) %}…{% endif %}
```
### Common option groups
| Prefix | Controls |
| - | - |
| `option.site.*` | Name, domain, HTTPS, landing page, verification policy |
| `option.brand.*` | Logos and favicon |
| `option.color_scheme.*` | Light and dark modes, accent colors |
| `option.features.*` | Feature switches, including the REST APIs |
| `option.billing.*` | Currency, usage mode, tax engine, fallback plan, trials |
| `option.credit_rate` | Per-rate credit pricing |
| `option.mail.*`, `option.smtp.*` | Mail transport |
| `option.cdn.*` | Storage adapter, URL signing, grouping |
| `option.embeddings.*` | Vector store adapter and embedding model |
| `option.policies.*` | Legal page contents |
| `option.script_tags.*` | Analytics and support widgets |
| `option.theme` | The active theme |
### Writing options
```php theme={null}
use Option\Application\Commands\SaveOptionCommand;
$this->dispatcher->dispatch(new SaveOptionCommand(
'acme',
json_encode(['api_key' => $key])
));
```
Writes merge into the existing JSON: nested objects are merged recursively, and lists are replaced. `DeleteOptionCommand` removes a key, including a nested one.
Administrator-facing forms post to the options API rather than calling this directly. See [Admin settings pages](/development/plugins/admin-settings-pages).
## Which layer to use
| Setting | Layer |
| - | - |
| Credentials for the installation itself, such as the database | Environment |
| Anything an administrator should change without editing files | Options |
| Paths and derived flags | Config |
Options require a database, so anything needed before the database exists, such as the debug flag, has to be an environment variable.
## Related
* [Dependency injection](/development/core/dependency-injection)
* [Bootstrap and lifecycle](/development/core/bootstrap-and-lifecycle)
* [Local development](/development/local-development)
# Console commands
Source: https://docs.aikeedo.com/development/core/console-commands
Every command available through Aikeedo's console, what it does, and which options it accepts.
```bash theme={null}
php bin/console list
php bin/console help app:locale:extract
```
The console boots the same application as a web request, so commands see the same configuration, services and plugins. Entity changes are flushed when the command finishes.
Without a database configured, only the three `app:locale:*` commands are registered. Everything else needs a connection.
## Application commands
### `app:import:seeds`
Imports the starter templates and categories shipped with the release, from `data/seeds`.
| Option | Purpose |
| - | - |
| `--set`, `-s` | Import only the named set; repeatable. Defaults to all. |
Also available as `app:import:presets`. See [Starter content](/advanced/seed-data).
### `app:locale:extract`
Scans sources and updates `.po` catalogs.
| Option | Purpose |
| - | - |
| `--target`, `-t` | `core` or a package name; repeatable |
| `--all`, `-a` | The application and every package |
| `--path`, `-p` | Scan an arbitrary directory, such as a theme repository |
| `--locale-dir` | Where `--path` writes catalogs |
| `--domain` | Override the gettext domain |
| `--language`, `-l` | Restrict to specific languages |
| `--no-prune` | Keep entries no longer present in the source |
| `--no-build` | Skip rebuilding the browser catalogs |
### `app:locale:translate`
Fills in missing translations using an AI model.
| Option | Purpose |
| - | - |
| `--lang`, `-l` | Languages to translate |
| `--all`, `-a` | Every language with a catalog |
| `--target`, `-t`, `--path`, `-p` | Same targeting as extraction |
| `--force`, `-f` | Retranslate entries that already have a translation |
| `--dry-run` | Show what would change |
| `--no-build` | Skip rebuilding the browser catalogs |
Model and API credentials are read from options and the environment.
### `app:locale:build`
Rebuilds the browser translation catalogs under the web root.
| Option | Purpose |
| - | - |
| `--lang`, `-l` | Specific languages |
| `--enabled`, `-e` | Only the enabled ones |
### `app:update`
Applies an update archive: backs up what must survive, extracts the release, runs migrations, reinstalls plugins, restores translations.
| Argument | Purpose |
| - | - |
| `file` | Path to the update archive |
Most installations update through the admin panel, which runs the same process. See [How to update](/versioning/how-to-update).
## Doctrine commands
### Migrations
| Command | Purpose |
| - | - |
| `migrations:status` | Current state |
| `migrations:migrate` | Apply pending migrations |
| `migrations:list` | List known migrations |
| `migrations:current`, `migrations:latest` | Current and latest versions |
| `migrations:execute` | Run or revert one migration |
| `migrations:generate`, `migrations:diff` | Create a migration, empty or from mapping differences |
| `migrations:dump-schema`, `migrations:rollup` | Squash history |
| `migrations:sync-metadata-storage` | Repair the tracking table |
| `migrations:up-to-date` | Exit code friendly check, useful in deployment scripts |
### ORM
| Command | Purpose |
| - | - |
| `orm:validate-schema` | Compare mappings with the database |
| `orm:info` | List mapped entities |
| `orm:mapping:describe` | Inspect one entity's mapping |
| `orm:schema-tool:create`, `update`, `drop` | Manipulate the schema directly |
| `orm:generate-proxies` | Regenerate proxy classes |
| `orm:run-dql` | Run a DQL query |
| `orm:clear-cache:metadata`, `query`, `result`, `region:*` | Clear Doctrine caches |
### DBAL
| Command | Purpose |
| - | - |
| `dbal:run-sql` | Run raw SQL |
`orm:schema-tool:update --force` changes the schema without recording a migration, which leaves the migration table out of step. Use migrations on anything but a scratch database.
## Running commands in production
* Run as the web server user, so cache and log files stay writable.
* Redirect output when running from cron; the console writes to standard output.
* Commands exit with a non-zero status on failure, so deployment scripts can check.
## Plugins and commands
Plugins can't register console commands: the command map is static. Expose the action as an admin endpoint, or run it from the cron event instead. See [Data and persistence](/development/plugins/data-and-persistence).
## Related
* [Database and migrations](/development/core/database-and-migrations)
* [Localization internals](/development/core/localization-internals)
* [Background jobs and cron](/development/core/background-jobs-and-cron)
# Database and migrations
Source: https://docs.aikeedo.com/development/core/database-and-migrations
How Aikeedo maps entities with Doctrine, identifies rows, paginates, and applies schema and data migrations.
Persistence is Doctrine ORM over MySQL. Mapping is done with attributes on entity classes in `src/`, and the schema is versioned with Doctrine Migrations.
## Mapping
```php theme={null}
#[ORM\Entity]
#[ORM\Table(name: 'category')]
#[ORM\HasLifecycleCallbacks]
class CategoryEntity
{
#[ORM\Embedded(class: Id::class, columnPrefix: false)]
private Id $id;
#[ORM\Embedded(class: Title::class, columnPrefix: false)]
private Title $title;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'created_at')]
private DateTimeInterface $createdAt;
}
```
Value objects are embeddables, so validation lives in the value object and the column stays flat.
Only `src/` is scanned for mappings. Plugins can't add entities. See [Data and persistence](/development/plugins/data-and-persistence).
## Identifiers
IDs are UUIDv7 values stored as binary. They're unique without a round trip, and they sort by creation time, which is what makes cursor pagination possible.
```php theme={null}
$id = $entity->getId(); // Shared\Domain\ValueObjects\Id
$string = (string) $id->getValue();
```
## Inheritance
Several tables use single-table inheritance with a discriminator column, so related kinds share one table:
| Table | Holds |
| - | - |
| `library_item` | Conversations, images, videos, speeches, transcriptions, isolated voices, classifications, memories and canvas artifacts |
| `file` | Generic files and images |
| `data_unit` | Knowledge base sources of different kinds |
| `stat` | Usage, signup and subscription statistics |
## Repositories
Domain code depends on repository interfaces; Doctrine implementations are bound during boot. The shared base class is immutable, so filters return a narrowed clone rather than mutating shared state:
```php theme={null}
$orders = $repo
->filterByWorkspace($workspace)
->filterByStatus(OrderStatus::COMPLETED)
->slice(0, 20);
```
Two behaviors come from the base:
* **Soft deletes.** Entities with a `deletedAt` field are excluded automatically.
* **Cursor pagination.** Results can start after, or end before, a known ID, which is what the API's `starting_after` and `ending_before` parameters use.
## When writes happen
Handlers call `add()` and entity methods; the entity manager is flushed once, after the response is emitted. Console commands and the cron entry point flush too.
Flush explicitly only when you must, such as when streaming a response that has to persist state before it finishes.
## Schema migrations
Migrations live in `migrations/mysql`, are namespaced `Migrations\MySql`, and are tracked in a `migration` table.
```bash theme={null}
php bin/console migrations:status
php bin/console migrations:migrate
php bin/console migrations:diff # generate from mapping changes
php bin/console migrations:execute --up 'Migrations\MySql\Version50000'
```
Files are named by the version they belong to, such as `Version50000` for 5.0.0, alongside timestamped ones for changes within a release.
`migrations:diff` compares the database against the entity mappings. Always read the generated SQL before running it: it will happily drop a column that an extension added outside the mapping.
## Data migrations
Schema changes aren't always enough: an update may need to reshape existing rows. Those live in `migrations/update` as classes implementing the migration interface, and are run in order by the migration manager, which records what it has run in the `migrated` option so each one runs once.
The update flow triggers them after the schema migration.
## Working with the schema
```bash theme={null}
php bin/console orm:validate-schema # mappings versus the database
php bin/console orm:info # list mapped entities
php bin/console orm:schema-tool:update --dump-sql
php bin/console dbal:run-sql "SELECT COUNT(*) FROM workspace"
```
`orm:schema-tool:update --force` bypasses migrations and leaves the migration table out of step with reality. Use migrations on anything but a scratch database.
## Tables
Core tables include `user`, `workspace`, `workspace_invitation`, `plan`, `plan_snapshot`, `subscription`, `order`, `coupon`, `option`, `category`, `preset`, `assistant`, `voice`, `file`, `library_item`, `message`, `artifact_version`, `data_unit`, `chatbot`, `chatbot_message`, `chatbot_conversation`, `contact`, `company`, `affiliate`, `payout`, `import_job`, `stat` and `migration`.
Adding columns to core tables is a fork: the next update's migrations don't know about them, and `migrations:diff` will propose dropping them. If you need extra data, see [Data and persistence](/development/plugins/data-and-persistence).
## Backups
Anything that changes the schema, including an update, should be preceded by a database dump. The update flow migrates in place, and there's no automatic rollback.
## Related
* [Modules and layers](/development/core/modules-and-layers)
* [Console commands](/development/core/console-commands)
* [How to update](/versioning/how-to-update)
# Dependency injection
Source: https://docs.aikeedo.com/development/core/dependency-injection
How Aikeedo's container autowires services, resolves configuration by ID, and lets modules and plugins bind their own implementations.
Aikeedo uses a [small autowiring container](https://github.com/iziphp/container). Most classes never register anything: they declare what they need in the constructor and the container builds it.
## Autowiring
```php theme={null}
class NoteService
{
public function __construct(
private NoteRepositoryInterface $repo,
private Dispatcher $dispatcher,
private LoggerInterface $logger,
) {}
}
```
Concrete classes are constructed recursively. Interfaces resolve when something has bound them, which module bootstrappers do during boot:
```php theme={null}
$app->set(CategoryRepositoryInterface::class, CategoryRepository::class);
```
## Injecting values by ID
Scalars and configuration come from the container by ID, using an attribute:
```php theme={null}
use Easy\Container\Attributes\Inject;
public function __construct(
#[Inject('option.billing.currency')]
private ?string $currency = 'USD',
#[Inject('config.dirs.uploads')]
private ?string $uploadsDir = null,
#[Inject('version')]
private string $version = 'dev',
) {}
```
Always pair `#[Inject('option.…')]` with a nullable type and a default. Options that were never saved don't resolve, and a non-nullable parameter then fails to construct the class.
## Resolvers
Two resolvers extend the container with dynamic IDs.
### Configuration
Anything starting with `config.` comes from the configuration object built at bootstrap:
| ID | Value |
| - | - |
| `config.dirs.root` | Installation root |
| `config.dirs.webroot` | Web root, honoring `PUBLIC_DIR` |
| `config.dirs.cache`, `.log`, `.uploads`, `.locale` | Runtime directories |
| `config.dirs.src`, `.views` | Source and template directories |
| `config.dirs.extensions`, `.artifacts` | Plugin directories |
| `config.enable_debugging` | Effective debug flag |
| `config.enable_caching` | Effective cache flag |
| `config.locale` | Default locale configuration |
### Options
Anything starting with `option.` comes from the options table. Values are stored as JSON, flattened into a dot map, and cast on the way out:
```php theme={null}
#[Inject('option.features.chat.is_enabled')]
private ?bool $chatEnabled = false,
```
| Stored | Resolved as |
| - | - |
| `"1"`, `"0"` | `int` |
| `"1.5"` | `float` |
| `"true"`, `"false"` | `bool` |
| A JSON object or array | `array` |
| Anything else | `string` |
Options are read once per request. If the database isn't configured, resolution quietly returns nothing, which keeps the installer working.
## Resolving imperatively
Where constructor injection isn't possible, such as inside a plugin's `boot()`:
```php theme={null}
use Application;
$isEnabled = Application::make('option.features.notes.is_enabled', false);
$dispatcher = Application::make(Dispatcher::class);
```
`Application::make($id, $default)` returns the default when the ID can't be resolved, instead of throwing.
Prefer constructor injection. Static resolution hides dependencies and makes classes harder to test; use it only for bootstrap-time decisions.
## Binding your own services
Bind an interface when callers should depend on the contract:
```php theme={null}
$app->set(NotifierInterface::class, EmailNotifier::class);
```
Bind an instance when it needs setting up before anything resolves it, such as passing configuration or registering entries:
```php theme={null}
$collection = new ToolCollection($container);
$collection->add(WeatherTool::LOOKUP_KEY, WeatherTool::class);
$app->set(ToolCollection::class, $collection);
```
## Keyed registries
For extension points with many implementations, Aikeedo uses registries rather than container bindings, so implementations can be listed as well as resolved:
| Registry | Holds |
| - | - |
| `Shared\Infrastructure\Collections\ServiceCollectionInterface` | Any keyed implementation of a given interface |
| `Billing\Infrastructure\Payments\PaymentGatewayFactoryInterface` | Payment gateways |
| `Shared\Infrastructure\FileSystem\CdnAdapterCollectionInterface` | Storage adapters |
| `Billing\Infrastructure\Currency\RateProviderCollectionInterface` | Exchange rate providers |
| `Ai\Infrastructure\Services\AiServiceFactory` | AI services |
| `Ai\Infrastructure\Services\Tools\ToolCollection` | Chat tools |
Entries are stored as class names and resolved from the container on first use, so registering is cheap.
## Lifetimes
Services resolved from the container are shared for the request. There's no request-scoped or transient lifetime to configure: a fresh process handles each request, and everything is discarded at the end.
## Related
* [Bootstrap and lifecycle](/development/core/bootstrap-and-lifecycle)
* [Configuration](/development/core/configuration)
* [Plugin dependency injection](/development/plugins/dependency-injection-and-services)
# Directory structure
Source: https://docs.aikeedo.com/development/core/directory-structure
What every directory in an Aikeedo installation contains, and which ones you can safely change.
```text theme={null}
aikeedo/
├── bin/console # Symfony Console entry point
├── bootstrap/ # autoloader, container and application bootstrap
├── config/ # providers, bootstrappers, commands, migrations, model registry
├── cron.php # scheduled work entry point
├── data/ # seed content shipped with the release
├── docs/ # OpenAPI specifications and the API docs viewer
├── extra/ # plugins and themes
├── helpers/ # global helper functions
├── locale/ # translation catalogs and the language catalogue
├── migrations/ # database and data migrations
├── public/ # web root
├── resources/ # templates, email templates, frontend sources, static files
├── src/ # application code
├── tests/ # test suites
├── var/ # runtime files: cache, logs, temporary data
├── vendor/ # Composer dependencies
├── .env # environment configuration
└── composer.json
```
## Application code
| Path | Contents |
| - | - |
| `src/{Module}/Domain` | Entities, value objects, repository interfaces, domain events and exceptions |
| `src/{Module}/Application` | Commands, command handlers, listeners and application services |
| `src/{Module}/Infrastructure` | Doctrine repositories, adapters, module bootstrappers |
| `src/Presentation` | Request handlers, middleware, responses, resources, validation, console commands |
| `src/Shared` | Command bus, container glue, filesystem, i18n, navigation, registries, caching |
| `src/Application.php` | The application object that boots providers and bootstrappers |
See [Modules and layers](/development/core/modules-and-layers).
## Bootstrap and configuration
| Path | Purpose |
| - | - |
| `bootstrap/autoload.php` | Registers Composer's autoloader and maps the application's namespaces to `src/` |
| `bootstrap/container.php` | Loads the environment, builds configuration, registers resolvers |
| `bootstrap/app.php` | Creates the application, adds providers and bootstrappers, boots |
| `config/providers.php` | Service providers, in order |
| `config/bootstrappers.php` | Module bootstrappers, in order |
| `config/commands.php` | The console command map |
| `config/migrations.php` | Doctrine Migrations configuration |
| `config/registry/base.json` | The AI model catalog shipped with the release |
| `config/registry/registry.json` | Per-installation model customizations. Not shipped, so updates preserve it. |
| `config/registry/import.json`, `servers.json` | Importable models and OpenAI-compatible servers |
## Views and frontend
| Path | Contents |
| - | - |
| `resources/views/layouts` | Base layouts: `base`, `main`, `minimal` |
| `resources/views/templates` | Pages, grouped as `admin`, `app`, `auth` and `install` |
| `resources/views/sections`, `snippets` | Shared blocks and partials |
| `resources/views/overrides` | Optional. Templates here take precedence over the originals. |
| `resources/emails` | Email templates, in the `@emails` namespace |
| `resources/assets` | JavaScript and CSS sources built by Vite |
| `resources/static` | Files copied to the web root as-is |
## Web root
| Path | Contents |
| - | - |
| `public/index.php` | Front controller |
| `public/assets`, `public/.vite` | Built frontend bundles and their manifest |
| `public/e/{vendor}/{package}` | Assets published by plugins and themes |
| `public/uploads` | Local file storage, when the local storage adapter is used |
| `public/locale` | Browser translation catalogs |
The web root's name comes from the `PUBLIC_DIR` environment variable, so hosts that serve from `public_html` work without changes.
## Plugins and themes
| Path | Contents |
| - | - |
| `extra/extensions/{vendor}/{package}` | Installed plugins and themes |
| `extra/artifacts` | Uploaded archives, used as a Composer artifact repository |
Both are referenced as Composer path repositories, which is how a locally placed package installs by name.
## Data and runtime
| Path | Contents |
| - | - |
| `data/seeds` | Starter content imported by `app:import:seeds` |
| `locale/{code}/LC_MESSAGES/messages.po` | The application's own translations |
| `locale/locale.json` | The list of languages and the default |
| `migrations/mysql` | Doctrine schema migrations |
| `migrations/update` | Data migrations run during an update |
| `var/cache` | Twig, route, metadata and PSR-6 caches |
| `var/log` | `app-*.log` and `error-*.log`, rotated daily |
| `var/imports`, `var/tmp` | Working files |
| `var/plugin-backup` | Backups taken before a plugin reinstall |
| `var/plugin-health.json` | Recorded plugin boot failures |
`var/` must be writable by the web server user, and so must the storage directory when the local adapter is used. A cron job running as a different user is a common cause of permission problems.
## What survives an update
| Path | On update |
| - | - |
| `src/`, `resources/` (except `overrides/`), `bootstrap/`, `config/*.php`, `migrations/`, `public/assets/` | Replaced |
| `vendor/` | Replaced, then plugin dependencies reinstalled |
| `config/registry/base.json` | Replaced. Customizations live in `registry.json`. |
| `extra/extensions/heyaikeedo/*` | Replaced with the bundled versions |
| `extra/extensions/{your-vendor}/*` | Kept and reinstalled |
| `resources/views/overrides/`, `locale/`, `.env`, `public/uploads/`, `var/` | Kept |
See [Extending Aikeedo](/development/extending-aikeedo) for what that means in practice.
## Quality tooling
| File | Tool |
| - | - |
| `phpstan.neon.dist` | Static analysis |
| `phpcs.xml.dist` | Coding standard |
| `phpmd.xml` | Mess detection |
| `phpunit.xml` | Tests |
| `vite.config.mjs`, `package.json` | Frontend build |
## Related
* [Bootstrap and lifecycle](/development/core/bootstrap-and-lifecycle)
* [Configuration](/development/core/configuration)
* [Views and frontend](/development/core/views-and-frontend)
# Email
Source: https://docs.aikeedo.com/development/core/email
How Aikeedo sends transactional email: transports, templates and the events that trigger messages.
Email is sent through Symfony Mailer, with templates rendered by Twig. Which transport is used depends on what an administrator configured.
## Transports
| `option.mail.transport` | Behavior |
| - | - |
| `smtp` | Sends over SMTP, using `option.smtp.host`, `port`, `username` and `password` |
| Anything else | A null transport that logs instead of sending |
The null transport is the default, which means a fresh installation silently drops mail until SMTP is configured. That's deliberate: it keeps a half-configured installation from emailing real users.
See [Mailer settings](/email/mailer) and [SMTP](/email/smtp) for the administrator side.
## Sending
```php theme={null}
use Shared\Infrastructure\Email\EmailService;
public function __construct(
private EmailService $email,
) {}
$this->email->sendTemplate(
$user->getEmail()->value,
'welcome',
['name' => $user->getFirstName()->value],
);
```
`sendTemplate()` takes one address or several, a template name, optional data and optional attachments. Every option is merged into the template data automatically, so templates can read `option.site.name`, brand settings and policy links without the caller passing them.
The sender comes from `option.mail.from.address` and `option.mail.from.name`.
## Templates
Templates live in `resources/emails` and are registered as the `@emails` Twig namespace.
| Template | Sent when |
| - | - |
| `layout.twig` | The shared shell the others extend |
| `welcome.twig` | A user registers, or verifies their address |
| `verify-email.twig` | A verification is requested, including after an address change |
| `password-reset.twig` | A recovery request is created |
| `workspace-invitation.twig` | Someone is invited to a workspace |
| `export.twig` | A data export is ready |
See [Email templates](/advanced/email-templates) for more details.
## What triggers mail
Transactional mail is sent by event listeners rather than by request handlers:
| Event | Message |
| - | - |
| `UserCreatedEvent` | Welcome, and verification |
| `EmailVerifiedEvent` | Welcome |
| `UserEmailUpdatedEvent` | Verification for the new address |
| `PasswordRecoveryCreatedEvent` | Password reset |
| `InvitationCreatedEvent` | Workspace invitation |
Because listeners run inside the request that dispatched the event, a slow or unreachable SMTP server slows that request down. Keep timeouts short, and prefer a nearby relay.
## Sending from a plugin
Inject the service and render your own template through your namespace:
```php theme={null}
$this->email->sendTemplate(
$user->getEmail()->value,
'@acme-notes/emails/digest.twig',
['notes' => $notes],
);
```
Extend the shared layout so your message matches the rest of the installation's mail, and check how the template resolves in your installation before relying on a namespaced path.
Don't send bulk mail through this path. It's for transactional messages, sent one at a time, inside a request. Marketing sends belong in a dedicated platform; see [Event-driven integrations](/development/plugins/guides/event-driven-integration).
## Testing
* Point SMTP at a local catcher, such as Mailpit or MailHog, while developing.
* With the null transport, messages are logged rather than sent: check `var/log/app-*.log`.
* Verify the "from" address is one your provider allows, or messages will be rejected or filtered.
## Related
* [Events](/development/core/events)
* [Mailer settings](/email/mailer)
* [Email templates](/advanced/email-templates)
# Events
Source: https://docs.aikeedo.com/development/core/events
Aikeedo's PSR-14 event system: how listeners are registered, how matching works, and the full catalog of domain events.
Domain events are how one part of the application reacts to another without depending on it. They're also the main extension point for plugins that need to act when something happens.
## Dispatching
```php theme={null}
use Psr\EventDispatcher\EventDispatcherInterface;
public function __construct(
private EventDispatcherInterface $dispatcher,
) {}
$this->dispatcher->dispatch(new CategoryCreatedEvent($category));
```
Dispatch is synchronous: listeners run in order, in the current request, before the call returns.
## Registering listeners
Three mechanisms feed the listener provider.
### On the event class
The core uses attributes on the event itself:
```php theme={null}
#[Listener(CreateSignupPlanSubscription::class)]
#[Listener(SendWelcomeEmail::class)]
#[Listener(SendVerificationEmail::class)]
#[Listener(TrackSignup::class)]
class UserCreatedEvent extends AbstractUserEvent {}
```
### Programmatically
Bootstrappers and plugins use the [array mapper](https://github.com/iziphp/event-dispatcher):
```php theme={null}
use Easy\EventDispatcher\Mapper\ArrayMapper;
use Easy\EventDispatcher\Priority;
$mapper->addEventListener(CreditUsageEvent::class, SaveStat::class);
$mapper->addEventListener(CronEvent::class, SyncContacts::class, Priority::LOW);
```
### On the listener class
A `#[Subscribe]` attribute on the listener is supported by the provider, though the core registers its own listeners the other two ways.
```php theme={null}
use Easy\EventDispatcher\Attributes\Subscribe;
use Easy\EventDispatcher\Priority;
#[Subscribe(UserCreatedEvent::class)]
#[Subscribe(UserDeletedEvent::class, Priority::LOW)]
class SyncCrmContact
{
public function __invoke(AbstractUserEvent $event): void
{
// ...
}
}
```
## Listeners
A listener is an invokable class, resolved from the container:
```php theme={null}
class SendWelcomeEmail
{
public function __construct(
private EmailService $email,
) {}
public function __invoke(UserCreatedEvent $event): void
{
$this->email->sendTemplate($event->user, 'welcome');
}
}
```
| Priority | Value | Runs |
| - | - | - |
| `Priority::HIGH` | 100 | First |
| `Priority::NORMAL` | 50 | Default |
| `Priority::LOW` | 0 | Last |
## Matching uses instanceof
A listener registered for a class also receives its subclasses. That's deliberate, and it has a sharp edge:
| Event | Extends |
| - | - |
| `UserUpdatedEvent` | `AbstractUserEvent` |
| `EmailVerifiedEvent` | `UserUpdatedEvent` |
| `UserEmailUpdatedEvent` | `UserUpdatedEvent` |
Registering one listener for both `UserUpdatedEvent` and `EmailVerifiedEvent` makes it run twice when an email is verified. Subscribe to the most general event you care about, and branch inside the listener if you need to tell them apart.
## Catalog
All classes live under `{Module}\Domain\Events`.
### User
| Event | Payload | Fires when |
| - | - | - |
| `UserCreatedEvent` | `$user` | An account is created |
| `UserUpdatedEvent` | `$user` | An account changes |
| `UserDeletedEvent` | `$user` | An account is deleted |
| `EmailVerifiedEvent` | `$user` | An email address is verified |
| `UserEmailUpdatedEvent` | `$user` | The email address changes |
| `UserPasswordUpdatedEvent` | `$user` | The password changes |
| `PasswordRecoveryCreatedEvent` | `$user` | A recovery request is created |
### Workspace
| Event | Payload |
| - | - |
| `WorkspaceCreatedEvent`, `WorkspaceUpdatedEvent`, `WorkspaceDeletedEvent` | `$workspace` |
| `InvitationCreatedEvent` | The invitation |
### Billing
| Event | Payload | Fires when |
| - | - | - |
| `OrderCreatedEvent` | `$order` | An order is created at checkout |
| `OrderFulfilledEvent` | `$order` | Payment is confirmed and the plan is granted |
| `SubscriptionCreatedEvent` | `$subscription` | A subscription starts |
| `SubscriptionActivatedEvent` | `$subscription` | A subscription becomes active |
| `SubscriptionUsageResetEvent` | `$subscription` | Usage resets, every 30 days |
| `PlanCreatedEvent`, `PlanUpdatedEvent`, `PlanDeletedEvent` | The plan | |
| `CouponCreatedEvent`, `CouponUpdatedEvent`, `CouponDeletedEvent` | The coupon | |
| `CreditUsageEvent` | `$workspace`, `$count` | Credits are consumed |
### Content and configuration
| Event | Payload |
| - | - |
| `CategoryCreatedEvent`, `CategoryUpdatedEvent`, `CategoryDeletedEvent` | The category |
| `PresetCreatedEvent`, `PresetUpdatedEvent`, `PresetDeletedEvent` | The template |
| `OptionCreatedEvent`, `OptionUpdatedEvent`, `OptionDeletedEvent` | The option |
### Chatbot
| Event | Payload |
| - | - |
| `ChatbotCreatedEvent`, `ChatbotUpdatedEvent`, `ChatbotDeletedEvent` | The chatbot |
| `ContactCreatedEvent`, `ContactUpdatedEvent`, `ContactDeletedEvent` | The contact |
| `ConversationCreatedEvent`, `ConversationUpdatedEvent`, `ConversationDeletedEvent` | The conversation |
| `DataUnitAttachedEvent`, `DataUnitDetachedEvent` | The data unit |
| `SecretKeyGeneratedEvent` | The chatbot |
### Cron
| Event | Payload |
| - | - |
| `Cron\Domain\Events\CronEvent` | None. Dispatched by `cron.php`. |
## What the core listens to
| Event | Core listeners |
| - | - |
| `UserCreatedEvent` | Create the signup plan subscription, send the welcome and verification emails, track the signup, record a statistic |
| `EmailVerifiedEvent` | Send the welcome email |
| `UserEmailUpdatedEvent` | Send a verification email |
| `PasswordRecoveryCreatedEvent` | Send the recovery email |
| `InvitationCreatedEvent` | Send the invitation email |
| `OrderFulfilledEvent` | Track an affiliate conversion |
| `SubscriptionUsageResetEvent` | Track a recurring affiliate conversion |
| `CreditUsageEvent`, `SubscriptionCreatedEvent`, `OrderCreatedEvent` | Record statistics |
| `CronEvent` | Renew subscriptions, end cancelled ones, calculate MRR, end failed generations, prune caches, purge expired library items, record the last run |
Most other events have no core listener: they exist so that plugins can react.
## Guidelines
Dispatch after the change, not before, so listeners see the final state.
Keep listeners fast, and catch your own exceptions, since a failure propagates into the request that dispatched the event.
Don't rely on listener order beyond priorities.
Remember entities aren't flushed yet: a listener sees objects in memory, not rows.
## Related
* [Command bus](/development/core/command-bus)
* [Background jobs and cron](/development/core/background-jobs-and-cron)
* [Plugin events and cron](/development/plugins/events-and-cron)
# Localization internals
Source: https://docs.aikeedo.com/development/core/localization-internals
How Aikeedo resolves a language per request, loads gettext catalogs for the core, themes and plugins, and builds browser dictionaries.
Translation uses gettext. The application, the active theme and every installed plugin each contribute a catalog, loaded per request for the resolved language.
## Resolving the language
`LocaleMiddleware` picks one, in this order:
A path such as `/de-DE/pricing`, when that language is enabled. A prefix for a disabled language redirects to the unprefixed path.
Their account language.
Set by a language switcher.
The browser's preference.
From the language configuration.
Languages themselves come from `locale/locale.json`, which lists each code, its English label, whether it's enabled, and its writing direction. Administrators enable languages and pick the default under **Settings → Languages**.
## Domains
| Source | Path | Domain |
| - | - | - |
| The application | `locale/{code}/LC_MESSAGES/messages.po` | `messages`, the default |
| The active theme | `extra/extensions/{theme}/locale/{code}/LC_MESSAGES/theme.po` | `theme` |
| Each plugin | `extra/extensions/{vendor}/{name}/locale/{code}/LC_MESSAGES/messages.po` | From the catalog's `X-Domain` header, falling back to `plugin` |
Plugins normally set `X-Domain: messages`, so their strings merge into the default domain and a plain `__()` resolves. Themes keep their own domain, which is why theme templates call `d__('theme', …)`.
## Translation functions
Registered globally, and as Twig functions and filters:
| Function | Purpose |
| - | - |
| `__(text)` | Default domain |
| `p__(context, text)` | With a context |
| `n__(singular, plural, count)` | Plural |
| `np__(context, singular, plural, count)` | Plural with a context |
| `d__(domain, text)` | Explicit domain |
| `dp__(domain, context, text)` | Domain and context |
| `dn__`, `dnp__` | Domain plurals |
| `noop__(text)` | Marks a string for extraction only |
Contexts matter more than they look: they let one English word be translated differently as a button and as a label.
## Browser catalogs
Strings used from JavaScript are compiled into per-language files under `public/locale`, with a content hash in the name. Templates reference the right one through a Twig function, and the catalog is regenerated when translations change or the cache is cleared.
## Commands
| Command | Purpose |
| - | - |
| `app:locale:extract` | Scan sources and update `.po` catalogs |
| `app:locale:translate` | Fill in missing entries with an AI model |
| `app:locale:build` | Rebuild the browser catalogs |
```bash theme={null}
php bin/console app:locale:extract --all
php bin/console app:locale:translate --target=acme/hello --all
php bin/console app:locale:build --enabled
```
Targets can be `core`, a package name, or an arbitrary path, which is what lets a theme be translated from its own repository. These three commands work even without a database configured.
## Updates preserve translations
An update backs up the locale directory, extracts the new release's strings, and merges the previous translations back in, so local edits aren't lost when the catalogs change.
## Related
* [Localization settings](/advanced/localization)
* [Plugin localization](/development/plugins/localization)
* [Theme localization](/development/themes/localization)
# Modules and layers
Source: https://docs.aikeedo.com/development/core/modules-and-layers
How Aikeedo organizes code into bounded contexts, and what belongs in the domain, application and infrastructure layers.
`src/` is divided by domain, not by technical role. Each module owns its data, its rules and the plumbing that persists them, which keeps billing logic out of the AI code and vice versa.
## A module
```text theme={null}
src/Billing/
├── Domain/
│ ├── Entities/ # OrderEntity, PlanEntity, SubscriptionEntity…
│ ├── ValueObjects/ # Price, BillingCycle, ExternalId…
│ ├── Repositories/ # OrderRepositoryInterface…
│ ├── Events/ # OrderCreatedEvent, SubscriptionCreatedEvent…
│ ├── Exceptions/ # AlreadyPaidException…
│ └── Services/ # BillingService, OrderFulfillmentService
├── Application/
│ ├── Commands/ # CreateOrderCommand, PayOrderCommand…
│ ├── CommandHandlers/ # one per command
│ └── Listeners/
└── Infrastructure/
├── BillingModuleBootstrapper.php
├── Repositories/DoctrineOrm/
├── Payments/ # gateway contracts and built-in gateways
├── Tax/
└── Currency/
```
`Presentation` is a module of its own rather than a layer inside each one, so every HTTP concern lives in one place.
## The domain layer
**Entities** are Doctrine-mapped classes that expose behavior rather than setters for every field:
```php theme={null}
#[ORM\Entity]
#[ORM\Table(name: 'category')]
class CategoryEntity
{
#[ORM\Embedded(class: Id::class, columnPrefix: false)]
private Id $id;
#[ORM\Embedded(class: Title::class, columnPrefix: false)]
private Title $title;
public function __construct(Title $title)
{
$this->id = new Id();
$this->title = $title;
$this->createdAt = new DateTimeImmutable();
}
}
```
Identifiers are UUIDv7 values stored as binary, which keeps them sortable by creation time while staying opaque.
**Value objects** validate in the constructor and are immutable, so an invalid value can't reach an entity:
```php theme={null}
$title = new Title('Marketing'); // throws InvalidValueException when empty
```
**Repository interfaces** live in the domain and are implemented in infrastructure, so handlers depend on the contract rather than Doctrine.
**Domain events** are plain objects carrying the entity they describe, dispatched after the change. See [Events](/development/core/events).
## The application layer
**Commands** are data holders that convert scalars into value objects and name their handler:
```php theme={null}
#[Handler(CreateCategoryCommandHandler::class)]
class CreateCategoryCommand
{
public Title $title;
public function __construct(string $title)
{
$this->title = new Title($title);
}
}
```
**Handlers** do the work and dispatch the event:
```php theme={null}
class CreateCategoryCommandHandler
{
public function __construct(
private CategoryRepositoryInterface $repo,
private EventDispatcherInterface $dispatcher,
) {}
public function handle(CreateCategoryCommand $cmd): CategoryEntity
{
$category = new CategoryEntity(title: $cmd->title);
$this->repo->add($category);
$this->dispatcher->dispatch(new CategoryCreatedEvent($category));
return $category;
}
}
```
Queries use the same pattern: `ReadPlanCommand`, `ListPlansCommand` and `CountPlansCommand` are commands whose handlers return data.
## The infrastructure layer
**Module bootstrappers** wire the module during boot: binding repository interfaces to implementations, registering navigation entries, and adding listeners.
**Doctrine repositories** extend a shared base that is deliberately immutable: each filter returns a clone with a narrowed query, so a repository can be passed around without one caller's filter leaking into another's.
```php theme={null}
$plans = $repo
->filterByStatus($status)
->orderBy('price', 'ASC')
->slice(0, 20);
```
Two behaviors come from the base class:
* **Soft deletes.** Entities with a `deletedAt` field are filtered out automatically.
* **Cursor pagination.** Results can be sliced relative to a known ID rather than an offset, which is what the API's `starting_after` and `ending_before` parameters use.
## Layering rules
| Layer | May depend on |
| - | - |
| Domain | Nothing outside its own module's domain |
| Application | Its own domain, and other modules' commands |
| Infrastructure | Everything, including third-party libraries |
| Presentation | Application commands and resources |
Treat this as the intended direction rather than an enforced rule: parts of the codebase reach across it. New code, including plugin code, is easier to maintain when it follows the direction.
## Related
* [Command bus](/development/core/command-bus)
* [Database and migrations](/development/core/database-and-migrations)
* [Events](/development/core/events)
* [Coding standards](/development/coding-standards)
# Core architecture
Source: https://docs.aikeedo.com/development/core/overview
The technology, structure and design principles behind the Aikeedo application, for developers who extend or customize it.
Aikeedo is a PHP application assembled from small PSR components rather than a full-stack framework. Reading this section pays off when you're writing a non-trivial plugin, tracing a bug, or deciding how to customize something safely.
This page applies to Aikeedo 5.x. Class names, paths and signatures match the 5.0 source code.
## Technology
| Concern | Choice |
| - | - |
| Language | PHP 8.2+, with the `intl`, `gd` and `bcmath` extensions |
| HTTP | PSR-7 messages and PSR-15 middleware, with Laminas Diactoros |
| Container | A small autowiring container with attribute-based injection |
| Routing | Attributes on request handler classes |
| Events | PSR-14, dispatched synchronously |
| Persistence | Doctrine ORM 3 and Doctrine Migrations on MySQL, with binary UUIDv7 IDs |
| Views | Twig 3, with gettext, Intl and Markdown extensions |
| HTTP client | PSR-18, backed by Symfony HttpClient |
| Console | Symfony Console |
| Cache and logs | Symfony Cache on the filesystem, and Monolog |
| Frontend | Alpine.js 3, Tailwind CSS 4 and Vite 8 |
## Design principles
`src/` is split by domain rather than by technical layer: `Ai`, `Billing`, `User`, `Workspace`, `Chatbot` and so on. Each owns its entities, repositories, commands and events.
Behavior is expressed as a command plus a handler, dispatched through a bus. Reads use the same mechanism, so `ListPlansCommand` and `CreateOrderCommand` look alike from the outside.
Entities take value objects, not scalars. Validation lives in the value object's constructor, so an invalid `Email` or `Price` can't exist.
Payment gateways, tax engines, storage adapters, vector stores, AI services and tools are all looked up from registries a plugin can add to.
Handlers change entities; the application flushes once, after the response is sent.
## The modules
| Module | Responsibility |
| - | - |
| `Ai` | Conversations, messages, generation, tools, embeddings, the model registry |
| `Assistant` | Assistant definitions |
| `Billing` | Plans, snapshots, orders, subscriptions, coupons, credits, gateways, tax |
| `Workspace` | Workspaces, members, invitations, credit balances |
| `User` | Accounts, authentication, preferences, single sign-on |
| `Affiliate` | Referrals, balances, payouts |
| `Category`, `Preset` | Content templates and their categories |
| `Chatbot` | Chatbot entities used by the chatbots feature |
| `Dataset` | Knowledge base sources and data units |
| `File` | File entities and image metadata |
| `Import` | Import jobs and migration adapters |
| `Option` | Settings storage and resolution |
| `Plugin` | Plugin and theme discovery, loading and lifecycle |
| `Presentation` | HTTP: request handlers, middleware, responses, resources, console commands |
| `Shared` | Container glue, command bus, filesystem, i18n, navigation, caching, registries |
| `Stat`, `Cron`, `Voice` | Statistics, scheduled work, voices |
## Request lifecycle in brief
```mermaid theme={null}
flowchart LR
subgraph Boot
direction TB
A[public/index.php] --> B[bootstrap/app.php]
B --> C[Container and configuration]
C --> D[Service providers]
D --> E[Module bootstrappers]
E --> F[Plugins boot]
end
subgraph Handle
direction TB
G[Route dispatch] --> H[Middleware pipeline]
H --> I[Request handler]
I --> J[Command bus]
J --> K[Domain and repositories]
end
subgraph Finish
direction TB
L[Response emitted] --> M[EntityManager flush]
end
Boot --> Handle --> Finish
```
Each step is covered in [Bootstrap and lifecycle](/development/core/bootstrap-and-lifecycle) and [Routing and middleware](/development/core/routing-and-middleware).
## Where to go next
What lives where in an installation.
Domain, application and infrastructure inside a module.
The full catalog of domain events.
Services, models, streaming and credits.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Routing and middleware
Source: https://docs.aikeedo.com/development/core/routing-and-middleware
How Aikeedo maps requests to handlers with attributes, assembles middleware pipelines, and turns exceptions into responses.
Routes aren't declared in a file. Each request handler carries attributes describing its path, method and middleware, and the router discovers them by scanning `src/` plus any directory a plugin or theme registers.
## Discovery
At bootstrap, an attribute mapper scans the source directory for classes with route attributes. The result is cached when `CACHE=true` and `DEBUG=false`.
```php theme={null}
use Easy\Router\Mapper\AttributeMapper;
$mapper->addPath(__DIR__);
```
That's also how plugins and themes add their own handlers. See [Routes and request handlers](/development/plugins/routes-and-request-handlers).
A new route on a cached installation isn't visible until the cache is cleared. Development should run with `CACHE=false`.
## Attributes
```php theme={null}
#[Path('/notes')]
#[Route(path: '/', method: RequestMethod::GET)]
#[Route(path: '/[uuid:id]', method: RequestMethod::PATCH, priority: Priority::HIGH)]
#[Middleware(SomeMiddleware::class)]
```
| Attribute | Effect |
| - | - |
| `Easy\Router\Attributes\Route` | Declares a path, an HTTP method and an optional priority. Repeatable. |
| `Easy\Router\Attributes\Path` | A prefix for the class and everything extending it. |
| `Easy\Router\Attributes\Middleware` | Adds PSR-15 middleware, outermost first. Repeatable. |
### Path syntax
| Pattern | Matches |
| - | - |
| `/notes` | Exactly that path |
| `/[:slug]` | One segment, exposed as the `slug` attribute |
| `/[uuid:id]` | A UUID segment |
| `/[images\|videos:type]` | One of the listed values |
| `/[locale:locale]?/pricing` | An optional locale prefix, such as `/de-DE` |
`locale` is a custom match type registered at bootstrap, matching language codes like `en-US`.
Route parameters arrive as request attributes:
```php theme={null}
$id = $request->getAttribute('id');
```
Higher priorities are matched first, which is how a specific route beats a catch-all.
## Groups
Handlers inherit their prefix and middleware from the class they extend:
| Base class | Prefix | Middleware added |
| - | - | - |
| `Presentation\RequestHandlers\AbstractRequestHandler` | none | Exception, Install, RequestBodyParser, User, Locale |
| `…\Api\Api` | `/api` | + Authorization, EmailVerification, Byok |
| `…\App\AppView` | `/app` | + Authorization, EmailVerification, View |
| `…\App\Billing\BillingView` | `/app/billing` | inherits `AppView` |
| `…\Admin\AbstractAdminRequestHandler` | `/admin` | + Authorization, admin role required |
| `…\Admin\AbstractAdminViewRequestHandler` | `/admin` | + DemoEnvironment, License, View |
| `…\Admin\Api\AdminApi` | `/admin/api` | + DemoEnvironment |
Handlers that extend none of these get a root path and no middleware at all, which is what webhook and embed endpoints use deliberately.
## The middleware pipeline
| Middleware | Responsibility |
| - | - |
| `ExceptionMiddleware` | Converts exceptions into responses, logs unhandled ones |
| `InstallMiddleware` | Routes everything to the installer while `ENVIRONMENT=install` |
| `RequestBodyParserMiddleware` | Parses JSON and form bodies into an object |
| `UserMiddleware` | Resolves the user from a token, cookie or API key, and the workspace |
| `LocaleMiddleware` | Resolves the language and loads translation catalogs |
| `AuthorizationMiddleware` | Requires an active user, and the admin role under `/admin` |
| `EmailVerificationMiddleware` | Enforces a strict verification policy |
| `ByokMiddleware` | Applies workspace-provided provider keys |
| `ViewMiddleware` | Renders a `ViewResponse` and injects template globals |
| `DemoEnvironmentMiddleware` | Blocks writes in demo mode |
| `LicenseMiddleware` | Gates admin views behind license verification |
| `CaptchaMiddleware` | Validates captcha tokens where enabled |
| `ClientIpMiddleware` | Resolves the client IP behind proxies |
Middleware from a parent class runs before a child's, so the outermost layer is always error handling.
## A request end to end
`POST /admin/api/categories`:
```mermaid theme={null}
sequenceDiagram
participant C as Client
participant R as Router
participant M as Middleware
participant H as CreateCategoryRequestHandler
participant B as Command bus
participant D as Domain
C->>R: POST /admin/api/categories
R->>M: matched handler + middleware
M->>M: exception, install, body parser, user, locale, authorization, demo
M->>H: handle(request)
H->>H: validate payload
H->>B: CreateCategoryCommand
B->>D: handler creates entity, dispatches event
D-->>H: CategoryEntity
H-->>C: 201 with CategoryResource
```
After the response is emitted, the entity manager is flushed.
## Responses
| Class | Use |
| - | - |
| `Presentation\Response\JsonResponse` | JSON payloads |
| `Presentation\Response\ViewResponse` | A Twig template plus data |
| `Presentation\Response\RedirectResponse` | Redirects |
| `Presentation\Response\HtmlResponse` | Raw HTML |
| `Presentation\Response\EmptyResponse` | No body |
| `Presentation\Response\Response` | A PSR-7 response you build yourself, including streams |
## Errors
`ExceptionMiddleware` maps exceptions to responses:
| Exception | Response |
| - | - |
| `ValidationException` | `400` with `code`, `message` and `param` |
| `UnauthorizedException` | `401` JSON on API paths, a redirect to `/login` elsewhere |
| `UsageLimitException`, `InsufficientCreditsException` | `403` |
| `HttpException` and its subclasses, such as `NotFoundException` | The status code it carries, defaulting to `422` |
| `InvalidValueException` | `400` |
| Anything else | Logged with a correlation ID, `500` JSON on API paths, an empty `500` elsewhere |
With `DEBUG` on, unhandled exceptions are rethrown instead, so you see the stack trace.
## Related
* [Authentication and authorization](/development/core/authentication-and-authorization)
* [Views and frontend](/development/core/views-and-frontend)
* [Plugin routes and handlers](/development/plugins/routes-and-request-handlers)
# Storage and files
Source: https://docs.aikeedo.com/development/core/storage-and-files
How Aikeedo stores generated media and uploads, selects a storage backend, and builds URLs.
Two filesystem services sit side by side: one for files users see, one for the installation's own working files.
| Service | Backed by | For |
| - | - | - |
| `Shared\Infrastructure\FileSystem\CdnInterface` | The selected storage adapter | Generated media, uploads, anything served to users |
| `Shared\Infrastructure\FileSystem\FileSystemInterface` | The local installation directory | Imports, temporary files, anything under `var/` |
Both are Flysystem instances, so the usual read, write, delete and listing methods apply.
## Selecting a backend
Adapters are registered in a collection during boot, and the administrator picks one under **Settings → File storage**. The key is stored in `option.cdn.adapter`, and the local adapter is the default and the fallback when a configured adapter can't be resolved.
The local adapter writes into the web root's `uploads` directory, so files are served directly. Other adapters are added by plugins. See [Storage adapters](/development/plugins/guides/storage-adapter).
## Writing a file
```php theme={null}
$key = $cdn->generatePath('png', $workspace, $user);
$cdn->write($key, $contents, ['visibility' => Visibility::PUBLIC]);
$url = $cdn->getUrl($key);
```
| Method | Purpose |
| - | - |
| `generatePath($ext, ?$workspace, ?$user)` | Builds a unique object key, applying the configured grouping |
| `write($path, $contents, $config)` | Stores the object |
| `getUrl($path)` | A public or signed URL, depending on configuration |
| `getAdapterLookupKey()` | The active adapter's key, stored with file records |
| `setVisibility($path, $visibility)` | Changes visibility after the fact |
### Grouping
`option.cdn.group_by` decides how object keys are namespaced: by workspace, by user, or by both. It keeps one tenant's objects together, which matters for auditing and bulk operations.
### Signed URLs
`option.cdn.sign_urls` asks adapters to return time-limited URLs instead of public ones.
Never store the result of `getUrl()`. Records keep the object key and the adapter key; the URL is derived when it's needed, because a signed one expires.
## File records
`File\Domain\Entities\FileEntity` records where a file lives: the storage adapter key, the object key, a URL and a size. `ImageFileEntity` extends it with width, height and a blurhash placeholder, which is what lets the interface show something before the image loads.
Because the adapter key is stored per file, switching backends doesn't break existing records, but it also doesn't move them: old files stay where they were written.
## Local working files
```php theme={null}
$fs->write('var/imports/' . $id . '.zip', $contents);
$stream = $fs->readStream('var/imports/' . $id . '.zip');
$fs->delete('var/imports/' . $id . '.zip');
```
Paths are relative to the installation root. `var/` isn't web-accessible, which is what makes it the right place for uploads being processed, exports being built and anything else that shouldn't be served.
## Permissions
The web server user must be able to write to `var/`, and to the uploads directory when the local adapter is used. A cron job running as a different user is the usual cause of files the web server can't later replace.
## Related
* [Files and storage in plugins](/development/plugins/files-and-storage)
* [Storage adapters guide](/development/plugins/guides/storage-adapter)
* [Cloud storage setup](/integrations/cloud-storage/overview)
* [Secure URLs](/advanced/secure-urls)
# Testing and code quality
Source: https://docs.aikeedo.com/development/core/testing-and-quality
The static analysis, coding standard and test tooling bundled with Aikeedo, and how to apply it to your own code.
The installation ships its own quality tooling, configured at the root. You can point all of it at plugin or theme code too.
## Composer scripts
| Command | Tool | Checks |
| - | - | - |
| `composer phpstan` | PHPStan | Static analysis of `src`, at level 5, excluding `src/Presentation` |
| `composer phpcs` | PHP\_CodeSniffer | PSR-1, PSR-12 and PHP 8.2 compatibility |
| `composer phpcbf`, `composer fix` | PHP\_CodeSniffer | Fixes what it can automatically |
| `composer phpmd` | PHPMD | Common code smells |
| `composer unit-test` | PHPUnit | The suite in `tests/` |
| `composer code-coverage` | PHPUnit | HTML coverage report in `coverage/` |
| `composer analyse` | All of the above | Static analysis, style, mess detection and tests, in order |
Configuration lives in `phpstan.neon.dist`, `phpcs.xml.dist`, `phpmd.xml` and `phpunit.xml`.
## Checking your own code
```bash theme={null}
vendor/bin/phpcs --standard=phpcs.xml.dist extra/extensions/acme/hello/src
vendor/bin/phpcbf --standard=phpcs.xml.dist extra/extensions/acme/hello/src
vendor/bin/phpstan analyse --level=5 extra/extensions/acme/hello/src
```
Static analysis is particularly valuable for plugins: it catches a renamed core method or a changed signature before your users do, which is the most common way a plugin breaks on a new release.
## Writing tests
The PHPUnit configuration bootstraps the application's autoloader and runs the suite in `tests/`, with coverage measured over `src/`.
For a plugin, test what you can isolate:
| Test | How |
| - | - |
| Value objects and mappers | Plain unit tests, no container needed |
| API clients | A PSR-18 client double returning canned responses |
| Services | Construct with test doubles for the core services they inject |
| Command handlers | Doubles for repositories and the event dispatcher |
Keep tests in your own package, with their own `composer.json` scripts, so they run without an Aikeedo installation.
```php tests/Unit/ContactMapperTest.php theme={null}
toContact($this->makeUser());
$this->assertSame('dev@acme.test', $contact['email']);
}
}
```
## What automated tests won't cover
Routes, settings pages, webhooks and payment flows need a running installation. Check them manually against a local install with `DEBUG=true`:
Install the extension into an installation that never had it.
Exercise every page and endpoint it adds.
Behave as a fresh installation would: no settings saved yet.
Deactivate and uninstall, and confirm nothing is left behind.
Read `var/log` afterwards.
## Continuous integration
A minimal pipeline for a plugin repository:
```yaml .github/workflows/ci.yml theme={null}
name: CI
on: [push, pull_request]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction
- run: vendor/bin/phpcs --standard=PSR12 src
- run: vendor/bin/phpstan analyse --level=5 src
- run: vendor/bin/phpunit
```
Pin the PHP version to the one Aikeedo requires, so incompatibilities surface in CI rather than on a customer's server.
## Related
* [Coding standards](/development/coding-standards)
* [Debugging plugins](/development/plugins/debugging)
* [Local development](/development/local-development)
# Views and frontend
Source: https://docs.aikeedo.com/development/core/views-and-frontend
How Aikeedo renders Twig templates, what globals every view receives, how the Vite-built frontend is wired, and how to override a core template.
Pages are server-rendered Twig, made interactive with Alpine.js. There's no separate single-page application: each page ships the markup it needs plus a small bundle.
## The Twig environment
The loader is set up with these paths, in order:
| Path | Namespace |
| - | - |
| `resources/views/overrides` | main, when the directory exists |
| `resources/views` | main |
| `resources/emails` | `@emails` |
| The active theme's directory | `@theme` |
| Directories plugins register | Whatever the plugin chooses |
Because `overrides` is first, a template placed there wins over the original of the same path.
| Setting | Behavior |
| - | - |
| `cache` | On only when caching is enabled and debug is off |
| `debug` | Follows `DEBUG` |
| `strict_variables` | Follows `DEBUG`, so undefined variables raise errors in development |
Extensions: Intl, gettext, Markdown, plus the application's own functions and the theme asset filter. The environment also exposes an `env` global holding the raw environment.
`env` contains credentials. A template may read one key from it, but it must never be dumped or serialized into the page.
## Rendering a view
A handler returns a `ViewResponse`, which `ViewMiddleware` renders:
```php theme={null}
return new ViewResponse('/templates/admin/plans.twig', ['plans' => $plans]);
```
The middleware merges the handler's data with a set of globals, so every view has the same context:
| Global | Contents |
| - | - |
| `option` | Every setting, as a nested map |
| `config` | Application configuration |
| `version`, `license` | Version and license |
| `theme`, `environment` | Active theme, and environment name |
| `nav` | The navigation registry |
| `view_namespace` | `app` or `admin`, which decides which JavaScript bundle loads |
| `currency` | The billing currency |
| `locales`, `locale` | Languages, and the resolved one |
| `user`, `workspace` | Present when someone is signed in |
Theme templates receive the same set. See [Template objects](/development/themes/template-objects).
## Layouts
| Layout | Used for |
| - | - |
| `layouts/base.twig` | The HTML shell: head, assets, blocks |
| `layouts/main.twig` | App and admin chrome: sidebar, header, content blocks |
| `layouts/minimal.twig` | Standalone pages, such as errors |
`main.twig` defines the blocks pages fill in, including `template` for content, `layout` for full-screen pages, and `styles` and `scripts` for extra assets. Plugin pages extend it. See [Frontend integration](/development/plugins/frontend-integration).
## The frontend build
Vite builds several entry points:
| Entry | Loaded on |
| - | - |
| `base` | Every page: web components, modal controller, toasts |
| `app` | `/app` pages |
| `admin` | `/admin` pages |
| `auth` | Sign-in and sign-up |
| `style`, `icons` | Stylesheets |
| `install` | The installer, when present |
Output goes to `public/` with a manifest at `public/.vite/manifest.json`, and `resources/static` is copied to the web root as-is. Templates resolve built files with the `asset` filter, which reads that manifest, or points at the dev server when `HMR` is enabled.
```twig theme={null}
```
## Interactivity
Alpine.js provides component behavior:
```twig theme={null}
{% set xdata = 'settings' %}
```
`main.twig` binds the page to the named Alpine component, which is registered in the relevant bundle. Shared behavior is packaged as custom elements, such as `x-form`, `x-money`, `x-avatar`, `x-copy` and `modal-element`, and as globals: an API client, a modal controller and a toast service.
The client talks to the JSON API rather than posting forms, which is why most admin pages are a Twig shell plus an Alpine component calling `/admin/api/...`.
## Overriding a core template
`resources/views/overrides/` doesn't exist by default. Create it, mirror the path of the template you want to change, and edit the copy:
```bash theme={null}
mkdir -p resources/views/overrides/templates/auth
cp resources/views/templates/auth/login.twig resources/views/overrides/templates/auth/login.twig
```
The directory isn't part of the release archive, so updates keep it. They do update the originals, though, and your copy won't pick up those changes.
Every override is a fork of one file. Keep them few, and compare each one against the updated original after every release.
## Emails
Email templates live in `resources/emails` under the `@emails` namespace, and are rendered by the mail service. See [Email](/development/core/email).
## Related
* [Routing and middleware](/development/core/routing-and-middleware)
* [Theme development](/development/themes/overview)
* [Frontend integration](/development/plugins/frontend-integration)
* [Localization internals](/development/core/localization-internals)
# Extending Aikeedo
Source: https://docs.aikeedo.com/development/extending-aikeedo
Choose between a plugin, a theme and a view override, and learn which customizations survive Aikeedo updates.
Aikeedo gives you several ways to change its behavior and appearance. They differ in what they can change, how you ship them, and whether an update keeps them. Pick the least invasive option that does what you need.
## Extension mechanisms
| Mechanism | Changes | Lives in | Survives updates |
| - | - | - | - |
| **Plugin** | Backend and frontend functionality: routes, admin and app pages, API endpoints, event listeners, cron jobs, navigation items, payment gateways, tax engines, storage adapters, vector stores, AI services and more | `extra/extensions/{vendor}/{name}` | Yes |
| **Theme** | The public landing page, plus any public pages the theme registers itself | `extra/extensions/{vendor}/{name}` | Yes, if the theme uses your own vendor name |
| **View override** | Any core Twig template, such as the login, signup, app or admin pages | `resources/views/overrides/` | Yes, but you must re-check it after each update |
| **Core modification** | Anything | `src/`, `resources/`, `config/`… | No, the next update overwrites it |
Don't edit core files unless you're willing to maintain a fork. An update extracts the new release over the installation root, which replaces every core file it contains.
## When to use a plugin
Use a plugin when you need to **add or change behavior**. A plugin is a Composer package of type `aikeedo-plugin`. Aikeedo loads it at boot and calls its entry class, which registers routes, templates, listeners and services with the core.
Typical plugins:
* Integrate a payment provider, tax service, CRM or email marketing tool
* Add a storage backend, vector database or AI provider
* Add pages to the app or admin panel, with their own API endpoints
* React to domain events such as a user signing up or an order being fulfilled
* Run scheduled work on every cron tick
Plugins can't add Doctrine entities, database migrations or console commands. See [Data and persistence](/development/plugins/data-and-persistence) for alternatives.
How plugins are discovered, booted and extended.
## When to use a theme
Use a theme when you need to **change the public website**. A theme is a Composer package of type `aikeedo-theme`. The active theme renders the landing page from its `templates/index.twig`, and it can register additional public pages through an optional PHP entry class.
A theme doesn't control the app, the admin panel, authentication pages, policy pages or error pages. Those templates belong to the core.
What a theme controls and how rendering works.
## When to use a view override
Use a view override when you need to **change a core template** that no plugin or theme controls, such as the login page layout.
Twig looks for templates in `resources/views/overrides/` before `resources/views/`. That directory doesn't exist by default. To override a template, copy it to the same relative path under `overrides/` and edit the copy:
```bash theme={null}
mkdir -p resources/views/overrides/templates/auth
cp resources/views/templates/auth/login.twig resources/views/overrides/templates/auth/login.twig
```
The release archive doesn't include `resources/views/overrides/`, so an update keeps your overrides. It still updates the originals, though, and your copy doesn't pick up those changes, such as new variables, fixes or markup that JavaScript depends on.
Keep overrides small and few. After each update, compare every override with its updated original, for example with `diff -u resources/views/templates/auth/login.twig resources/views/overrides/templates/auth/login.twig`.
If template caching is enabled (`CACHE=true` with `DEBUG=false`), clear the cache after you add or change an override. Use **Status → Clear cache** in the admin panel.
## What an update replaces
An update extracts the new release archive over the installation root, runs database migrations, then reinstalls every installed plugin and theme through Composer. The table shows how each location is affected.
| Location | During an update |
| - | - |
| `src/`, `resources/views/` (except `overrides/`), `resources/assets/`, `bootstrap/`, `config/*.php`, `migrations/`, `public/assets/` | Replaced with the new release |
| `vendor/` | Replaced, then plugin dependencies are reinstalled |
| `config/registry/base.json` | Replaced. Model customizations live in `config/registry/registry.json`, which the release doesn't include |
| `extra/extensions/heyaikeedo/default`, `public/e/heyaikeedo/default` | Replaced with the new default theme |
| `extra/extensions/{your-vendor}/*` | Kept, and reinstalled from its package |
| `resources/views/overrides/` | Kept |
| `locale/` | Your translations are backed up and merged back into the new catalogs |
| `.env`, `public/uploads/`, `var/` | Kept |
Never customize the bundled `heyaikeedo/default` theme in place, because the next update overwrites it. Copy it under your own vendor name first. See [Customizing existing themes](/development/themes/customizing-existing-themes).
## Decision checklist
Build a plugin that registers a payment gateway. Start with [Payment gateways](/development/plugins/guides/payments/overview).
Build a theme. Add the extra page with a theme entry class, as described in [Custom pages](/development/themes/custom-pages).
Build a plugin with a request handler that extends `Presentation\RequestHandlers\App\AppView`. See [App pages and APIs](/development/plugins/app-pages-and-apis).
Use a view override for `templates/auth/signup.twig`, and re-check it after each update.
Build a plugin that listens to `User\Domain\Events\UserCreatedEvent`. See [Event-driven integrations](/development/plugins/guides/event-driven-integration).
Use the [REST API](/development/api/overview). You don't need to change Aikeedo itself.
## Related
* [Plugin quickstart](/development/plugins/quickstart)
* [Theme quickstart](/development/themes/quickstart)
* [Views and frontend](/development/core/views-and-frontend)
* [How to update](/versioning/how-to-update)
# Aikeedo developer documentation
Source: https://docs.aikeedo.com/development/introduction
Technical documentation for extending, customizing and integrating a self-hosted Aikeedo installation with plugins, themes, the REST API and the core architecture.
Aikeedo is a self-hosted PHP application, and you get its full source code. This section is for developers who want to extend an installation, change how it looks, integrate it with other systems, or understand how the core works before changing it.
This page applies to Aikeedo 5.x. Class names, paths and signatures match the 5.0 source code.
## What you can build
Add features, pages, API endpoints, payment gateways, tax engines, storage adapters, AI providers and integrations, all without editing core files.
Design the public landing page and add your own marketing pages with Twig, Tailwind CSS and Alpine.js.
Connect external applications to the User and Admin REST APIs, including streamed chat responses.
Learn the architecture, request lifecycle, modules and subsystems before you override or extend core behavior.
## Choose where to start
Install Aikeedo locally, then follow [Local development](/development/local-development) to turn on debug mode, frontend hot reloading and cron.
Read [Extending Aikeedo](/development/extending-aikeedo) to decide between a plugin, a theme and a view override, and to see which changes survive updates.
Follow the [plugin quickstart](/development/plugins/quickstart) or the [theme quickstart](/development/themes/quickstart). Both take you from an empty directory to working code in an installation.
## Technology at a glance
| Layer | Technology |
| - | - |
| Language | PHP 8.2+ with `intl`, `gd` and `bcmath` |
| Application framework | Lightweight PSR components from the [`iziphp`](https://github.com/iziphp) packages (`Easy\*`): [DI container](https://github.com/iziphp/container), [attribute router](https://github.com/iziphp/router), [PSR-14 events](https://github.com/iziphp/event-dispatcher), [PSR-15 middleware](https://github.com/iziphp/http-server-handler) |
| Persistence | Doctrine ORM 3 and Doctrine Migrations on MySQL, with binary UUIDv7 identifiers |
| Views | Twig 3 with gettext translation functions |
| Frontend | Alpine.js 3, Tailwind CSS 4 and Vite 8 for the app and admin panel |
| Themes | Twig, Alpine.js, Tailwind CSS 3 and Vite 5 in the official starter kit |
| Tooling | Symfony Console, Composer, PHPStan, PHP\_CodeSniffer, PHPMD, PHPUnit |
See [Core architecture](/development/core/overview) for the full picture.
## Conventions in these docs
* **Paths** are relative to the Aikeedo installation root, the directory that contains `composer.json`, `bin/` and `src/`. The web root is `public/` by default, or whatever the `PUBLIC_DIR` environment variable names.
* **Class names** are fully qualified. Core classes have no vendor prefix; for example, `Billing\Infrastructure\Payments\PaymentGatewayInterface` lives in `src/Billing/Infrastructure/Payments/PaymentGatewayInterface.php`.
* **Examples** use a fictional vendor, `acme`, with packages such as `acme/hello` and the namespace `Acme\Hello\`. Replace them with your own vendor and package names.
* **Commands** run from the installation root unless a page says otherwise, for example `php bin/console app:locale:extract`.
## Prerequisites
To follow the guides, you should be comfortable with:
* Modern PHP: namespaces, attributes, enums, readonly properties and Composer
* Twig templates and basic Alpine.js
* Working in a terminal with `php`, `composer` and `npm`
Develop and test extensions on a local or staging copy, not on your production installation.
## Related
* [Local installation](/setup/installation/local)
* [Server requirements](/overview/server-requirements)
* [How to update](/versioning/how-to-update)
# Local development environment
Source: https://docs.aikeedo.com/development/local-development
Configure an Aikeedo installation for development: debug mode, the Vite dev server, hot module replacement, cron, and the console.
This page turns a working local installation into a development environment: verbose errors, no caching, rebuilt frontend assets on save, and a cron runner.
This page applies to Aikeedo 5.x. Class names, paths and signatures match the 5.0 source code.
## Prerequisites
* A local Aikeedo installation. If you don't have one, follow the [local installation guide](/setup/installation/local) first.
* PHP 8.2 or newer with the `intl`, `gd` and `bcmath` extensions
* MySQL 8.0 or newer
* Composer 2
* Node.js 20.19+ or 22.12+ with npm, needed only for frontend work
```bash theme={null}
php -v && composer -V && node -v && npm -v
```
The release archive ships with `vendor/` already installed, so you don't need `composer install` to run Aikeedo. It does not ship `node_modules/`, so you need `npm install` before you can run the Vite dev server.
## Step 1: Configure the environment
Edit `.env` in the installation root:
```ini .env theme={null}
ENVIRONMENT=dev
DEBUG=true
CACHE=false
HMR=1
```
| Variable | Effect in development |
| - | - |
| `ENVIRONMENT` | `dev` marks the installation as a development environment. Other values are `prod`, `install` and `demo`. |
| `DEBUG` | Shows errors instead of a blank `500`, disables Twig and route caches, and makes Twig strict about undefined variables. It also rethrows plugin boot errors instead of hiding them. |
| `CACHE` | Turn off template, route, listener and Doctrine metadata caching so changes apply immediately. |
| `HMR` | `1` makes the app load its JavaScript and CSS from the Vite dev server instead of the built files in `public/assets`. |
| `ASSETS_SERVER` | Where the app looks for those assets. Defaults to `http://localhost:5173`. |
| `THEME_ASSETS_SERVER` | Same idea for theme assets, normally `http://localhost:5174`. Set it only while you run a theme dev server. |
Never leave `DEBUG=true` on a production site. Debug mode exposes stack traces and disables caching.
## Step 2: Start the frontend dev server
Aikeedo builds the app, admin and auth bundles with Vite.
```bash theme={null}
npm install
npm run dev
```
Vite serves on port 5173. Keep the process running: it rebuilds on save and reports build errors.
```text theme={null}
VITE v8.0.16 ready in 412 ms
➜ Local: http://localhost:5173/
```
You only need Vite when you change files under `resources/assets`. For PHP or Twig work, leave `HMR=0` and use the prebuilt assets in `public/assets`.
### How assets resolve
| Directory | Purpose |
| - | - |
| `resources/assets/js/{base,app,admin,auth}` | JavaScript entry points, one per bundle |
| `resources/assets/css` | Stylesheets, including the Tailwind CSS entry |
| `resources/static` | Files copied to the web root as-is |
| `public/assets` + `public/.vite/manifest.json` | Build output and the manifest Twig reads |
Templates resolve asset URLs through the `asset` Twig filter. With `HMR` enabled, that filter points at `ASSETS_SERVER` instead of the manifest.
## Step 3: Serve the application
Use PHP's built-in server for local work:
```bash theme={null}
php -S 0.0.0.0:8000 -t public
```
Open `http://localhost:8000`. Run this in a second terminal so Vite keeps running in the first.
The built-in server is single-threaded. Streamed responses, such as chat, occupy the process while they run, so a second request can appear to hang. Use `php -S` with a worker-capable setup, Docker, Valet or nginx with PHP-FPM if that gets in your way.
## Step 4: Run cron
Scheduled work, including subscription renewals, usage resets, statistics and import jobs, runs through `cron.php`. Nothing schedules it for you locally, so run it by hand when you need it:
```bash theme={null}
php cron.php
```
To mirror production, add a crontab entry that runs it every minute. See [Initial setup](/setup/initial-setup) for the production configuration.
## Step 5: Use the console
`bin/console` exposes the Symfony Console application, including Doctrine's commands:
```bash theme={null}
php bin/console list
```
Commands you'll use often in development:
| Command | Purpose |
| - | - |
| `php bin/console migrations:migrate` | Apply pending database migrations |
| `php bin/console migrations:status` | Show the current migration state |
| `php bin/console orm:validate-schema` | Check entity mappings against the database |
| `php bin/console app:locale:extract` | Extract translatable strings into catalogs |
| `php bin/console app:import:seeds` | Import the bundled starter content |
See [Console commands](/development/core/console-commands) for the full list.
## Development workflow
* **PHP changes** apply on the next request. If you add or move a route, clear the route cache or keep `CACHE=false`.
* **Twig changes** apply immediately while caching is off.
* **Asset changes** are rebuilt by Vite and pushed to the browser.
* **Plugin and theme changes** may need a cache clear, and a new plugin needs `composer require`. See the [plugin quickstart](/development/plugins/quickstart) and [theme quickstart](/development/themes/quickstart).
Clear caches at any time from **Status → Clear cache** in the admin panel, which also rebuilds the browser translation catalogs.
## Build for production
```bash theme={null}
npm run build
```
This writes optimized bundles to `public/assets`, copies `resources/static` into the web root, and refreshes `public/.vite/manifest.json`. Set `HMR=0`, `DEBUG=false` and `CACHE=true` before you deploy.
## Troubleshooting
Vite isn't running, or `ASSETS_SERVER` doesn't match its port. Start `npm run dev`, or set `HMR=0` to use the built assets.
Caching is on. Set `CACHE=false`, or clear the cache from **Status → Clear cache**.
Debug mode enables strict variables. Guard optional values with `{% if foo is defined %}`.
Routes are cached when `CACHE=true` and `DEBUG=false`. Turn caching off during development, or clear the cache.
Logs are in `var/log/app-YYYY-MM-DD.log` and `var/log/error-YYYY-MM-DD.log`. Check that `var/` is writable by your PHP user.
## Related
* [Coding standards](/development/coding-standards)
* [Core architecture](/development/core/overview)
* [Bootstrap and lifecycle](/development/core/bootstrap-and-lifecycle)
* [Troubleshooting](/setup/troubleshooting)
# Admin settings pages
Source: https://docs.aikeedo.com/development/plugins/admin-settings-pages
Add a settings screen to the Aikeedo admin panel, save values through the options API, and read them back in PHP and Twig.
Plugins configure themselves through the admin panel. You add a page under `/admin/settings/...`, render a form with the core layout, and let Aikeedo's settings component save the values as options.
## The page
```php src/RequestHandlers/SettingsRequestHandler.php theme={null}
$this->mode,
]);
}
}
```
`AbstractAdminViewRequestHandler` puts the route under `/admin` and applies the admin middleware, so non-administrators never reach it.
## The template
```twig templates/settings.twig theme={null}
{% extends "/layouts/main.twig" %}
{% set active_menu = 'settings' %}
{% set xdata = 'settings' %}
{% block title p__('title', 'Hello settings')|title %}
{% block template %}
{% endblock %}
```
### What each piece does
| Piece | Purpose |
| - | - |
| `{% extends "/layouts/main.twig" %}` | Uses the admin chrome: sidebar, header and asset tags |
| `{% set active_menu = 'settings' %}` | Highlights **Settings** in the sidebar |
| `{% set xdata = 'settings' %}` | Binds the page to Aikeedo's `settings` Alpine component, which owns `submit()` and `isProcessing` |
| `` | A web component that styles and manages form state |
| `@submit.prevent="submit"` and `x-ref="form"` | Hand the form to the component; both are required |
| `:processing="isProcessing"` | Shows a spinner on the button while saving |
## How values are stored
`submit()` posts the form to `/admin/api/options`. Each **top-level** field name becomes one option key, and the nested values become its JSON payload:
| Field name | Option key | Read it as |
| - | - | - |
| `hello[api_key]` | `hello` | `option.hello.api_key` |
| `hello[modes][]` | `hello` | `option.hello.modes` |
| `features[hello][is_enabled]` | `features` | `option.features.hello.is_enabled` |
Saving merges your fields into the existing option, so you can split settings across several pages without wiping the other page's values.
Namespace your options with your package name, such as `hello`, unless you're extending an existing group like `features`.
### Reading values back
```php theme={null}
use Easy\Container\Attributes\Inject;
public function __construct(
#[Inject('option.hello.api_key')]
private ?string $apiKey = null,
#[Inject('option.hello.is_enabled')]
private ?bool $isEnabled = false,
) {}
```
In Twig, every option is available under `option`:
```twig theme={null}
{{ option.hello.api_key ?? '' }}
{% if option.hello.is_enabled is defined and option.hello.is_enabled %}
```
Checkboxes submit `1` or `0`, which is cast to a boolean when injected. An option nobody saved yet resolves to `null`, so always provide defaults.
### Writing values from PHP
```php theme={null}
use Option\Application\Commands\SaveOptionCommand;
$this->dispatcher->dispatch(new SaveOptionCommand(
'hello',
json_encode(['sync' => ['status' => 'processing']])
));
```
## Link the page from the admin panel
Add a navigation item in `boot()` so administrators can find the page, and set `extra.default_url` in your manifest so the plugins list links to it:
```php theme={null}
$item = new Item('/admin/settings/hello', p__('nav', 'Hello'), 'confetti');
$item->description = p__('nav', 'Connect your Acme account');
$item->isBuiltIn = false;
$this->nav->item('admin.settings.common', $item);
```
See [Navigation](/development/plugins/navigation) for the available sections.
Extension points that the core lists for administrators follow a URL convention: the key you register the implementation under becomes the settings URL, such as `/admin/settings/payments/{key}`, `/admin/settings/tax-engines/{key}`, `/admin/settings/cdn/{key}` and `/admin/settings/vector-databases/{key}`. Declare a route at that exact path, and point `default_url` at it.
## Handling secrets
* Use `type="password"` with `autocomplete="new-password"`.
* Never echo a secret into JavaScript or a data attribute.
* Show connection state, such as "key configured", rather than the value itself, where you can.
## Test and live credentials
A common pattern is one `mode` select plus two credential sets, with hidden inputs mirroring the selected set into the flat keys your PHP reads:
```twig theme={null}
```
Your PHP then injects `option.hello.api_key` and doesn't care which mode is active.
## Multi-page settings
For a larger integration, split the pages and redirect until setup is complete:
```php theme={null}
#[Route(path: '/plugins/acme/hello', method: RequestMethod::GET)]
class OverviewRequestHandler extends AbstractAdminViewRequestHandler implements
RequestHandlerInterface
{
public function __construct(
#[Inject('option.hello.api_key')]
private ?string $apiKey = null,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
if (!$this->apiKey) {
return new RedirectResponse('/admin/plugins/acme/hello/keys');
}
return new ViewResponse('@acme-hello/overview.twig');
}
}
```
Use `{% set active_menu = '/admin/plugins' %}` on those templates so the sidebar highlights **Plugins**.
## Troubleshooting
The form is missing `x-ref="form"` or `@submit.prevent="submit"`, or `xdata` isn't set to `settings`.
Options are injected when the handler is constructed. Reload the page. If it still shows stale data, clear the cache.
You saved a top-level key that another page also owns. Keep one top-level key per plugin, and merge rather than replace.
## Related
* [Data and persistence](/development/plugins/data-and-persistence)
* [Navigation](/development/plugins/navigation)
* [Frontend integration](/development/plugins/frontend-integration)
# Using AI models and credits
Source: https://docs.aikeedo.com/development/plugins/ai-models-and-credits
Call AI models from an Aikeedo plugin, check plan access, and reserve, consume and release workspace credits correctly.
A plugin can use the AI services the installation already has configured, instead of holding its own API keys. Aikeedo resolves a service for the model, and the billing service handles credits.
## The flow
```mermaid theme={null}
sequenceDiagram
participant P as Your plugin
participant W as WorkspaceEntity
participant B as BillingService
participant F as AiServiceFactory
participant S as Provider service
P->>W: isModelGranted(model)
P->>B: assertCanConsume(workspace, model)
P->>B: reserve(workspace, estimate, model)
P->>F: create(MessageServiceInterface::class, model)
F-->>P: provider service
P->>S: generate
S-->>P: output + usage
P->>B: release(workspace, estimate, model)
P->>B: consume(workspace, cost, model)
```
Reserving before the call stops a workspace from starting more work than its balance allows, then you release the estimate and consume the real cost once you know it.
## Check access first
```php theme={null}
use Ai\Domain\Exceptions\ModelNotAccessibleException;
use Ai\Domain\ValueObjects\Model;
$model = new Model('gpt-4o');
if (!$workspace->isModelGranted($model)) {
throw new ModelNotAccessibleException($model->value);
}
// Throws when the workspace is out of credits or past its usage window.
$this->billing->assertCanConsume($workspace, $model);
```
`isModelGranted()` reflects the workspace's plan. `assertCanConsume()` throws `Ai\Domain\Exceptions\InsufficientCreditsException` or `Billing\Domain\Exceptions\UsageLimitException`, which the exception middleware turns into a `403`.
## Resolve a service
```php theme={null}
use Ai\Domain\Completion\TextCompletionServiceInterface;
use Ai\Domain\Services\AiServiceFactoryInterface;
public function __construct(
private AiServiceFactoryInterface $factory,
) {}
$service = $this->factory->create(TextCompletionServiceInterface::class, $model);
$response = $service->complete(
$model,
'You summarize notes in one sentence.',
$noteBody,
maxTokens: 256,
);
```
The factory returns the first registered service that implements the interface and supports the model, and throws when nothing matches.
| Capability interface | Use for |
| - | - |
| `Ai\Domain\Completion\TextCompletionServiceInterface` | A single prompt-and-answer completion |
| `Ai\Domain\Completion\MessageServiceInterface` | Streamed chat, returns a `Generator` of stream parts |
| `Ai\Domain\Image\ImageServiceInterface` | Image generation |
| `Ai\Domain\Video\VideoServiceInterface` | Video generation |
| `Ai\Domain\Speech\SpeechServiceInterface` | Text to speech |
| `Ai\Domain\Transcription\TranscriptionServiceInterface` | Speech to text |
| `Ai\Domain\Classification\ClassificationServiceInterface` | Classification |
| `Ai\Domain\Embedding\EmbeddingServiceInterface` | Embeddings |
| `Ai\Domain\IsolatedVoice\VoiceIsolatorServiceInterface` | Voice isolation |
## Bill the usage
```php theme={null}
use Ai\Infrastructure\Services\CostCalculator;
public function __construct(
private CostCalculator $calculator,
private BillingService $billing,
) {}
$estimate = $this->calculator->estimate($model);
$this->billing->reserve($workspace, $estimate, $model);
try {
$response = $service->complete($model, $instructions, $input);
} finally {
// Always give the reservation back, then charge what it really cost.
$this->billing->release($workspace, $estimate, $model);
}
$this->billing->consume($workspace, $response->cost, $model);
```
Most service responses carry the cost the provider service already calculated, such as `TextCompletionResponse::$cost`, so you rarely compute it yourself.
| Method | Effect |
| - | - |
| `assertCanConsume(WorkspaceEntity, ?Model)` | Throws if the workspace can't spend right now |
| `reserve(WorkspaceEntity, CreditCount, ?Model)` | Holds an estimated amount for the duration of the call |
| `release(WorkspaceEntity, CreditCount, ?Model)` | Returns the held amount |
| `consume(WorkspaceEntity, CreditCount, ?Model)` | Charges the real cost and dispatches `CreditUsageEvent` |
Always pass the `Model`. The billing service uses it to decide whether credits apply at all: when the workspace brings its own provider key, nothing is charged.
### Cost calculation
Use the calculator when you bill something the core doesn't price for you. `CostCalculator::estimate($model)` returns the model's configured multiplier as a rough pre-flight number, and `CostCalculator::calculate($amount, $model, $opt)` converts real usage into credits using the installation's configured rates. The optional bitmask selects a rate variant, such as `CostCalculator::INPUT`, `CostCalculator::OUTPUT`, `CostCalculator::IMAGE`, `CostCalculator::QUALITY_HD` and the `SIZE_*` constants.
```php theme={null}
$inputCost = $this->calculator->calculate($inputTokens, $model, CostCalculator::INPUT);
$outputCost = $this->calculator->calculate($outputTokens, $model, CostCalculator::OUTPUT);
```
## Streaming chat
`MessageServiceInterface::generateMessage()` returns a generator of stream parts. Fold them as they arrive, and take the cost from the usage part:
```php theme={null}
use Ai\Domain\ValueObjects\Stream\TextDeltaPart;
use Ai\Domain\ValueObjects\Stream\UsagePart;
use Billing\Domain\ValueObjects\CreditCount;
$cost = new CreditCount(0);
$text = '';
foreach ($service->generateMessage($model, $message) as $part) {
if ($part instanceof TextDeltaPart) {
$text .= $part->delta;
}
if ($part instanceof UsagePart) {
// Each usage part carries the running total, so overwrite it.
$cost = $part->cost;
}
}
$this->billing->consume($workspace, $cost, $model);
```
To forward that stream to a browser, see [Streaming responses](/development/plugins/streaming-responses).
## Which models exist
The model registry holds every service and model the installation knows about, including admin customizations:
```php theme={null}
use Shared\Infrastructure\Services\ModelRegistry;
public function __construct(
private ModelRegistry $registry,
) {}
foreach ($this->registry['directory'] as $service) {
foreach ($service['models'] as $model) {
// $model['key'], $model['name'], $model['type'], $model['enabled']
}
}
```
Let administrators pick a model in your settings page rather than hardcoding one, and fall back to a related global setting, such as `option.embeddings.model`, where one exists.
## Adding your own provider
Everything above consumes services the core registered. To add a provider, implement a capability interface and register it with the factory. See [AI providers](/development/plugins/guides/ai-provider).
## Related
* [Streaming responses](/development/plugins/streaming-responses)
* [AI providers guide](/development/plugins/guides/ai-provider)
* [AI subsystem internals](/development/core/ai-subsystem)
* [Unified credit system](/billing/unified-credit-system)
# App pages and workspace APIs
Source: https://docs.aikeedo.com/development/plugins/app-pages-and-apis
Add pages and JSON endpoints for signed-in Aikeedo users, scope them to the current workspace, and enforce access control.
Plugins can extend the part of Aikeedo that users see: pages under `/app` and JSON endpoints under `/api`. Both run inside a workspace, so every read and write has to be scoped to it.
## A page under /app
```php src/RequestHandlers/NotesView.php theme={null}
isEnabled) {
throw new NotFoundException();
}
return new ViewResponse('@acme-notes/templates/notes.twig');
}
}
```
`AppView` prefixes `/app`, requires an authenticated user, enforces the email verification policy and renders `ViewResponse` through Twig. The example above is served at `GET /app/notes`.
### The template
```twig templates/notes.twig theme={null}
{% extends "/layouts/main.twig" %}
{% set active_menu = '/app/notes' %}
{% set xdata = 'notes' %}
{% block title p__('title', 'Notes')|title %}
{% block template %}
{{ p__('heading', 'Notes') }}
{% endblock %}
```
| Variable | Effect |
| - | - |
| `active_menu` | Highlights your entry in the sidebar; use the same URL you registered in navigation |
| `xdata` | Names the Alpine component that backs the page |
| `nav_mode = 'settings'` | Renders the settings sidebar instead of the primary one, for pages under **Settings** |
| `{% block template %}` | Your content inside the standard chrome |
| `{% block layout %}` | Replaces the whole layout, for full-screen pages such as editors |
See [Frontend integration](/development/plugins/frontend-integration) for the Alpine and asset side.
## A workspace-scoped JSON endpoint
```php src/RequestHandlers/Api/ListNotesRequestHandler.php theme={null}
getAttribute(WorkspaceEntity::class);
$cmd = new ListNotesCommand();
$cmd->setWorkspace($workspace);
$res = new ListResource();
foreach ($this->dispatcher->dispatch($cmd) as $note) {
$res->pushData(new NoteResource($note));
}
return new JsonResponse($res);
}
}
```
With a shared base class for the group:
```php src/RequestHandlers/Api/NotesApi.php theme={null}
getAttribute(UserEntity::class);
$workspace = $request->getAttribute(WorkspaceEntity::class);
```
* The workspace is the caller's current workspace, or the one named by the `X-Workspace-Id` header when the caller is a member of it.
* The workspace attribute is set **only** when the user belongs to it, so treat a missing workspace as an error rather than a reason to fall back.
## Access control
Scope every query by workspace, and verify ownership whenever an ID comes from the request.
```php src/AccessControls/NoteAccessControl.php theme={null}
getWorkspace()->hasUser($user)) {
// Treat someone else's resource as missing, not forbidden:
// a 403 would confirm that the id exists.
throw new NotFoundException(param: 'note');
}
return $note;
}
}
```
The core uses the same shape in `Presentation\AccessControls\*`, with `Presentation\AccessControls\Permission` for finer-grained checks. Two conventions worth copying:
* A resource the caller may not see is a `404`, not a `403`.
* A resource that exists but is withheld by the user's plan is a `403`.
Checking the parent resource isn't enough for nested routes. On a route such as `/api/notes/{noteId}/comments/{commentId}`, verify that the comment belongs to that note **and** that the note belongs to the caller's workspace. Skipping the second check lets anyone read another workspace's data by guessing IDs.
## Feature flags and plan limits
Gate your feature behind an option so administrators can turn it off:
```php theme={null}
#[Inject('option.features.notes.is_enabled')]
private ?bool $isEnabled = false,
```
Enforce per-plan limits yourself, reading the plan configuration from the workspace's subscription. To expose your own per-plan settings in the admin panel, see [Plan config extensions](/development/plugins/guides/plan-config-extension).
## Write endpoints
* Validate with `Presentation\Validation\Validator::validateRequest()`.
* Return `StatusCode::CREATED` with the created resource, or `EmptyResponse` with `StatusCode::NO_CONTENT` for deletes.
* Changes are flushed at the end of the request, so persist through a repository and return.
```php theme={null}
use Easy\Http\Message\StatusCode;
use Presentation\Response\EmptyResponse;
return new EmptyResponse(StatusCode::NO_CONTENT);
```
## Related
* [Routes and request handlers](/development/plugins/routes-and-request-handlers)
* [Frontend integration](/development/plugins/frontend-integration)
* [Navigation](/development/plugins/navigation)
* [Authentication internals](/development/core/authentication-and-authorization)
# Custom extension points
Source: https://docs.aikeedo.com/development/plugins/custom-extension-points
Let other plugins extend yours by publishing your own interfaces through the service collection and the AI service factory.
The mechanisms Aikeedo uses to stay extensible are available to your plugin too. Publish an interface, let implementations register themselves under a key, and resolve them at runtime.
## Publish an interface
Define the contract in your package, and document it:
```php src/Domain/NotificationChannelInterface.php theme={null}
services->add(
EmailChannel::LOOKUP_KEY,
EmailChannel::class,
NotificationChannelInterface::class
);
}
```
| Method | Purpose |
| - | - |
| `add(string $key, string\|object $service, string $type)` | Registers a class or instance under a key for a type. It asserts the class implements that type. |
| `get(string $key, string $type)` | Resolves one implementation, lazily, from the container |
| `ofType(string $type)` | Iterates every implementation of that type |
Services are resolved from the container when they're first used, so registering is cheap even if the implementation is expensive to build.
## Consume implementations
```php theme={null}
use Acme\Alerts\Domain\NotificationChannelInterface;
public function __construct(
private ServiceCollectionInterface $services,
#[Inject('option.alerts.channel')]
private ?string $channelKey = null,
) {}
public function notify(string $subject, string $body): void
{
// One selected implementation
$channel = $this->services->get(
$this->channelKey ?? 'email',
NotificationChannelInterface::class
);
$channel->send($subject, $body);
}
/** @return iterable */
public function available(): iterable
{
// Everything registered, for an admin picker
foreach ($this->services->ofType(NotificationChannelInterface::class) as $channel) {
if ($channel->isEnabled()) {
yield $channel;
}
}
}
```
`get()` throws `Shared\Infrastructure\Collections\ServiceNotFoundException` when the key isn't registered, so fall back to a default rather than letting a stale setting break the page.
## Let others configure their implementation
Copy the convention the core uses for its own extension points: the registration key doubles as the settings URL. If your plugin lists channels at `/admin/settings/alerts`, link each one to `/admin/settings/alerts/{key}`, and let the implementing plugin declare that route and point its `extra.default_url` there. See [Admin settings pages](/development/plugins/admin-settings-pages).
## Load order
Plugins boot in the order they're discovered, so don't assume another plugin has registered before you. Resolve implementations when you use them, not in `boot()`, and treat an empty collection as normal.
## Custom AI capabilities
The AI service factory works the same way. Define a capability interface that extends the base AI contract, then register services for it:
```php src/Domain/SummaryServiceInterface.php theme={null}
factory->register(OpenAiSummaryService::class);
}
// Later: the first registered service that implements the interface
// and supports the model.
$service = $this->factory->create(SummaryServiceInterface::class, $model);
```
Implementations still provide `supportsModel()` and `getSupportedModels()` from `Ai\Domain\Services\AiServiceInterface`, which is how the factory picks one.
## Document the contract
An extension point is an API. Ship it like one:
* Publish the interface's signatures and semantics in your README.
* Version the contract, and follow semantic versioning when it changes.
* Say which key namespace implementers should use, such as `acme-alerts.*`.
* Provide one reference implementation inside your own package.
## Related
* [Dependency injection](/development/plugins/dependency-injection-and-services)
* [Events and cron](/development/plugins/events-and-cron)
* [AI providers guide](/development/plugins/guides/ai-provider)
# Data and persistence
Source: https://docs.aikeedo.com/development/plugins/data-and-persistence
Where an Aikeedo plugin can store data: options, core entities and external stores, and why plugins cannot ship their own database migrations.
Plugins can't add their own Doctrine entities or migrations, because the ORM maps only the core `src/` directory and the migration paths are fixed. That's less limiting than it sounds: most plugins need settings and a little state, and both have a home.
## Choose a storage strategy
| You need to store | Use |
| - | - |
| Settings, credentials, feature flags | Options |
| Job state, cursors, counters | Options |
| Data that belongs to an existing concept, such as a note on a user | Core entity metadata |
| Records that belong to a workspace and grow without bound | Your own external store, or a table you create and manage yourself |
| Files | The CDN or the local filesystem, see [Files and storage](/development/plugins/files-and-storage) |
## Options
Options are key-value rows holding JSON. They're loaded once per request and exposed to the container, so reading one is free after boot.
### Reading
```php theme={null}
use Easy\Container\Attributes\Inject;
public function __construct(
#[Inject('option.crm.api_key')]
private ?string $apiKey = null,
#[Inject('option.crm.sync')]
private ?array $sync = null,
) {}
```
In Twig, the same values are under `option`:
```twig theme={null}
{{ option.crm.api_key ?? '' }}
```
### Writing
```php theme={null}
use Option\Application\Commands\SaveOptionCommand;
use Shared\Infrastructure\CommandBus\Dispatcher;
$this->dispatcher->dispatch(new SaveOptionCommand(
'crm',
json_encode([
'sync' => ['status' => 'processing', 'cursor_id' => null],
])
));
```
Writes **merge**, they don't replace. Associative arrays are merged recursively, so the call above leaves `option.crm.api_key` untouched. Lists are replaced wholesale, which is what you want for an array of configured items.
```php theme={null}
use Option\Application\Commands\DeleteOptionCommand;
$this->dispatcher->dispatch(new DeleteOptionCommand('crm'));
```
One top-level key per plugin. Two pages writing different subtrees of the same key is fine, but two plugins sharing a key will overwrite each other's structure.
Options are global to the installation, not per workspace or per user. For per-workspace state, key the data inside the option by workspace ID, or store it on a core entity.
## Core entities
Your plugin can read and write the core's own data through its commands and repositories, which keeps validation and events intact:
```php theme={null}
use Shared\Infrastructure\CommandBus\Dispatcher;
use Workspace\Application\Commands\ReadWorkspaceCommand;
$workspace = $this->dispatcher->dispatch(new ReadWorkspaceCommand($workspaceId));
```
Entities are persisted at the end of the request: the application flushes the entity manager once, after the response is emitted. Call the entity's methods and let the flush happen.
```php theme={null}
use Doctrine\ORM\EntityManagerInterface;
public function __construct(
private EntityManagerInterface $em,
) {}
// Only when you must persist before the request ends, such as mid-stream.
$this->em->flush();
```
Several core entities support metadata, which is a practical place for a small amount of plugin-owned data attached to an existing record. Check the entity for an `addMeta()` or `setMeta()` method before inventing storage of your own.
## When you really need tables
If your plugin genuinely needs its own tables, own them end to end:
* Create the schema from an `install()` hook, using the DBAL connection from the entity manager, and drop it in `uninstall()`.
* Query with DBAL rather than the ORM, since Doctrine won't map your entities.
* Namespace table names with your vendor, such as `acme_notes`, so you never collide with a future core table.
```php theme={null}
use Doctrine\ORM\EntityManagerInterface;
public function install(Context $context): void
{
$this->em->getConnection()->executeStatement(<<<'SQL'
CREATE TABLE IF NOT EXISTS acme_notes (
id BINARY(16) NOT NULL PRIMARY KEY,
workspace_id BINARY(16) NOT NULL,
title VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL
) DEFAULT CHARACTER SET utf8mb4
SQL);
}
```
Aikeedo won't migrate, back up or delete tables you create, and nothing validates them against a future release. Prefer options or an external service unless you're prepared to maintain the schema yourself.
## Caching
For values that are expensive to compute or fetch, use the PSR-6 cache pool instead of a new option:
```php theme={null}
use Psr\Cache\CacheItemPoolInterface;
public function __construct(
private CacheItemPoolInterface $cache,
) {}
public function lists(): array
{
$item = $this->cache->getItem('acme_crm.lists');
if (!$item->isHit()) {
$item->set($this->client->fetchLists());
$item->expiresAfter(60);
$this->cache->save($item);
}
return $item->get();
}
```
Clearing the application cache also clears this pool, so never treat it as durable storage.
## Cleaning up
Uninstalling a plugin removes its files, but not its data. Delete what you own in the `uninstall()` hook:
```php theme={null}
public function uninstall(Context $context): void
{
$this->dispatcher->dispatch(new DeleteOptionCommand('crm'));
}
```
## Related
* [Lifecycle and hooks](/development/plugins/lifecycle-and-hooks)
* [Files and storage](/development/plugins/files-and-storage)
* [Events and cron](/development/plugins/events-and-cron)
* [Database internals](/development/core/database-and-migrations)
# Debugging plugins
Source: https://docs.aikeedo.com/development/plugins/debugging
Diagnose Aikeedo plugins that don't load, don't boot or misbehave, using debug mode, logs and the plugin health record.
Aikeedo protects a production site from a broken plugin: boot errors are swallowed, fatals are recorded, and the plugin is skipped on later requests. That's the opposite of what you want while developing, so start by turning debug mode on.
## Turn on debug mode
```ini .env theme={null}
ENVIRONMENT=dev
DEBUG=true
CACHE=false
```
With `DEBUG=true`:
* Exceptions thrown while loading or booting a plugin are rethrown instead of hidden.
* Plugins previously recorded as failed are booted anyway, so you can see the error again.
* Twig and route caches are off, so template and route changes apply immediately.
* Twig runs with strict variables, and undefined variables raise errors instead of rendering nothing.
## Plugin health
When debug mode is off and a plugin causes a fatal error during boot, the failure is written to `var/plugin-health.json`, and the admin panel shows the plugin as **failed**. It stays skipped until someone reactivates it, which clears the record.
```bash theme={null}
cat var/plugin-health.json
```
The entry names the package, the error, the file and line, and the application version at the time. Reactivating the plugin from **Plugins** clears it; so does deleting the file.
Only fatal errors, such as a parse error or a missing class, are recorded this way. A catchable exception in `boot()` is skipped silently in production, which is why development should always run with `DEBUG=true`.
## Logs
| File | Contents |
| - | - |
| `var/log/app-YYYY-MM-DD.log` | Everything the application logs, including your `LoggerInterface` calls |
| `var/log/error-YYYY-MM-DD.log` | Errors only, including unhandled exceptions with a correlation ID |
An unhandled exception in production returns `{"message": "Internal error", "id": "..."}`. Search that ID in the error log to find the stack trace.
```php theme={null}
use Psr\Log\LoggerInterface;
public function __construct(
private LoggerInterface $logger,
) {}
$this->logger->info('Acme sync started', ['workspace' => (string) $workspace->getId()]);
```
## Common problems
The manifest failed validation, or the package isn't where Aikeedo looks.
* The directory must be `extra/extensions/{vendor}/{name}` and match the `name` field.
* `type` must be `aikeedo-plugin`.
* `require` must include `heyaikeedo/composer`.
* `extra.entry-class` must be set.
An invalid manifest raises `InvalidPluginComposerJsonFileException`, which debug mode surfaces.
Composer doesn't know about your package.
```bash theme={null}
composer require acme/hello
composer dump-autoload
```
Check that `autoload.psr-4` maps your namespace to `src/`, and that the namespace in `extra.entry-class` matches the class exactly, including escaped backslashes in JSON.
The entry class exists but doesn't implement `Plugin\Domain\PluginInterface`. Implement it and define `boot(Context $context): void`.
`boot()` runs on the request **after** activation. Reload the page. If it still doesn't run, the plugin may be marked as failed: check `var/plugin-health.json` and the admin panel.
* Did you call `AttributeMapper::addPath()` for the directory that holds the handler?
* Does the handler implement `Psr\Http\Server\RequestHandlerInterface`?
* Is the route cached? Set `CACHE=false`, or clear the cache from **Status → Clear cache**.
* Is the full path what you expect? The base class adds a prefix such as `/admin` or `/api`.
The namespace passed to `FilesystemLoader::addPath($dir, 'acme-hello')` must match the `@acme-hello/...` reference, the path must exist, and the plugin must be active so `boot()` ran.
The option hasn't been saved yet, or the dot path is wrong. `#[Inject('option.hello.api_key')]` matches the JSON under the option key `hello`. Confirm what was stored by dumping `option.hello` in a template.
The form needs `x-ref="form"`, `@submit.prevent="submit"` and `{% set xdata = 'settings' %}`. Check the browser network tab for the `POST /admin/api/options` request.
Files listed in `extra.public` are copied at install time. Re-run `composer require acme/hello`, or copy the file into `public/e/{vendor}/{name}/` while iterating.
A fatal error during boot, with debug mode off and a stale health record missing. Check `var/log/error-*.log`, then remove the plugin directory or run `composer remove acme/hello` to recover.
## Useful checks
```bash theme={null}
# Is the package installed and autoloadable?
composer show acme/hello
# Does the console boot the application cleanly?
php bin/console list
# What did the last cron tick do?
php cron.php && tail -n 50 var/log/app-$(date +%F).log
```
Booting the console exercises the same bootstrap path as a web request, so a plugin that breaks boot usually breaks `bin/console` too, with a visible stack trace.
## Before you release
The plugin installs cleanly into an installation that never had it.
Activating, deactivating and uninstalling all work, and uninstalling leaves nothing behind.
The plugin behaves when its settings are empty, since that's the state right after install.
## Related
* [Lifecycle and hooks](/development/plugins/lifecycle-and-hooks)
* [Local development](/development/local-development)
* [Packaging and distribution](/development/plugins/packaging-and-distribution)
# Dependency injection and core services
Source: https://docs.aikeedo.com/development/plugins/dependency-injection-and-services
How an Aikeedo plugin receives core services through constructor injection, which services are available, and how to use the command bus.
Aikeedo resolves your entry class, request handlers, listeners and command handlers from its dependency injection container. Anything you type-hint in a constructor is built and injected for you, and that is how a plugin reaches the application.
## Constructor injection
```php src/Plugin.php theme={null}
loader->addPath(__DIR__ . '/../templates', 'acme-hello');
$this->mapper->addPath(__DIR__);
}
}
```
The container autowires concrete classes and any interface the core has bound to an implementation. Your own classes are autowired too, so a service of yours can depend on another service of yours without registration.
## Injecting configuration and settings
Scalar values come from the container by ID, using the `#[Inject]` attribute:
```php theme={null}
use Easy\Container\Attributes\Inject;
public function __construct(
#[Inject('option.hello.greeting')]
private ?string $greeting = null,
#[Inject('option.hello.is_enabled')]
private ?bool $isEnabled = false,
#[Inject('config.dirs.uploads')]
private ?string $uploadsDir = null,
) {}
```
| ID prefix | Resolves to |
| - | - |
| `option.*` | A stored setting, by dot path, cast to `int`, `float` or `bool` where possible |
| `config.dirs.*` | Absolute paths: `root`, `webroot`, `cache`, `log`, `src`, `views`, `uploads`, `locale`, `extensions`, `artifacts` |
| `config.enable_debugging`, `config.enable_caching` | The effective debug and cache flags |
| `version`, `license` | The application version and license string |
| `option.theme` | The active theme's package name |
Always give injected options a default and a nullable type. An option that was never saved resolves to `null`, and settings pages are often empty on a fresh installation.
Inside `boot()` you can also read a value imperatively:
```php theme={null}
use Application;
if (Application::make('option.features.hello.is_enabled', false)) {
// register optional pieces
}
```
## Services a plugin commonly injects
| Service | Use it for |
| - | - |
| `Twig\Loader\FilesystemLoader` | Register a template namespace with `addPath($dir, 'acme-hello')` |
| `Easy\Router\Mapper\AttributeMapper` | Register routes with `addPath($dir)` |
| `Easy\EventDispatcher\Mapper\ArrayMapper` | Subscribe listeners to domain events |
| `Shared\Infrastructure\Navigation\Registry` | Add menu entries |
| `Shared\Infrastructure\Collections\ServiceCollectionInterface` | Register implementations of an extension-point interface |
| `Shared\Infrastructure\CommandBus\Dispatcher` | Dispatch core and your own commands |
| `Psr\EventDispatcher\EventDispatcherInterface` | Dispatch events |
| `Psr\Http\Client\ClientInterface` | Call external HTTP APIs |
| `Psr\Http\Message\RequestFactoryInterface`, `StreamFactoryInterface`, `UriFactoryInterface` | Build PSR-7 requests |
| `Psr\Log\LoggerInterface` | Write to `var/log` |
| `Psr\Cache\CacheItemPoolInterface` | Cache expensive lookups |
| `Shared\Infrastructure\FileSystem\CdnInterface` | Store and serve user-visible files |
| `Shared\Infrastructure\FileSystem\FileSystemInterface` | Work with local files under the installation root |
| `Billing\Domain\Services\BillingService` | Check and consume credits |
| `Ai\Domain\Services\AiServiceFactoryInterface` | Resolve an AI service for a model |
| `Presentation\Validation\Validator` | Validate request payloads |
| `Doctrine\ORM\EntityManagerInterface` | Flush mid-request, for example while streaming |
## Calling an external API
Use the PSR-18 client the application already configures, instead of bundling your own HTTP library:
```php src/Client.php theme={null}
$payload */
public function post(string $path, array $payload): object
{
if (!$this->apiKey) {
throw new RuntimeException('Acme API key is not configured.');
}
$request = $this->requestFactory
->createRequest('POST', self::BASE_URL . $path)
->withHeader('Authorization', 'Bearer ' . $this->apiKey)
->withHeader('Content-Type', 'application/json')
->withBody($this->streamFactory->createStream(
safe_json_encode($payload)
));
$response = $this->client->sendRequest($request);
if ($response->getStatusCode() >= 300) {
throw new RuntimeException(
'Acme API error: ' . $response->getStatusCode()
);
}
return json_decode((string) $response->getBody());
}
}
```
`safe_json_encode()` is a global helper the application autoloads. It encodes without throwing on invalid UTF-8.
## The command bus
Aikeedo separates intent from execution. A command is a data object tagged with the handler that executes it, and `Dispatcher::dispatch()` resolves the handler from the container and runs it synchronously.
### Dispatching core commands
```php theme={null}
use Shared\Infrastructure\CommandBus\Dispatcher;
use User\Application\Commands\ReadUserCommand;
use Workspace\Application\Commands\ReadWorkspaceCommand;
$user = $this->dispatcher->dispatch(new ReadUserCommand($userId));
$workspace = $this->dispatcher->dispatch(new ReadWorkspaceCommand($workspaceId));
```
### Defining your own
```php src/Application/Commands/SendGreetingCommand.php theme={null}
client->post('/greetings', ['email' => $cmd->email]);
}
}
```
Dispatching an unhandled command throws `Shared\Infrastructure\CommandBus\Exception\NoHandlerFoundException`. There is no queue: handlers run in the current request.
## Registering your own services
You rarely need to register anything, because the container autowires concrete classes. Register explicitly when you want to bind an interface to an implementation that other plugins can swap, or when you're publishing an extension point. See [Custom extension points](/development/plugins/custom-extension-points).
## Related
* [Routes and request handlers](/development/plugins/routes-and-request-handlers)
* [Data and persistence](/development/plugins/data-and-persistence)
* [Command bus internals](/development/core/command-bus)
* [Dependency injection internals](/development/core/dependency-injection)
# Events and scheduled work
Source: https://docs.aikeedo.com/development/plugins/events-and-cron
React to Aikeedo domain events from a plugin, dispatch your own, and run batched background work on the cron tick.
Aikeedo dispatches domain events whenever something meaningful happens: a user signs up, an order is fulfilled, credits are consumed. Plugins subscribe to those events, and use the cron event for scheduled work.
## Subscribe to an event
Register listeners in `boot()` through the event mapper:
```php src/Plugin.php theme={null}
mapper->addEventListener(UserCreatedEvent::class, PushContact::class);
$this->mapper->addEventListener(UserUpdatedEvent::class, PushContact::class);
$this->mapper->addEventListener(CronEvent::class, SyncContacts::class);
}
}
```
## Write a listener
A listener is an invokable class. It's resolved from the container, so it can depend on any service:
```php src/Listeners/PushContact.php theme={null}
autoSync) {
return;
}
$user = $event->user;
try {
$this->client->upsertContact([
'email' => (string) $user->getEmail()->value,
'first_name' => (string) $user->getFirstName()->value,
]);
} catch (Throwable $th) {
// Never let an integration break the user's request.
$this->logger->error('CRM sync failed', ['exception' => $th]);
}
}
}
```
Listeners run synchronously inside the request that dispatched the event. Keep them fast, and catch your own exceptions, or a failing third-party API will break signup. Log failures instead of swallowing them silently, and reconcile later from cron.
### Priorities
```php theme={null}
use Easy\EventDispatcher\Priority;
$this->mapper->addEventListener(UserCreatedEvent::class, Welcome::class, Priority::HIGH);
```
`Priority::HIGH` (100) runs before `Priority::NORMAL` (50), which runs before `Priority::LOW` (0).
### Inheritance matters
Listeners match with `instanceof`, so a listener on a parent class also receives its subclasses.
| Event | Extends |
| - | - |
| `UserUpdatedEvent` | `AbstractUserEvent` |
| `EmailVerifiedEvent` | `UserUpdatedEvent` |
| `UserEmailUpdatedEvent` | `UserUpdatedEvent` |
Registering the same listener on both `UserUpdatedEvent` and `EmailVerifiedEvent` makes it run **twice** when an email is verified. Subscribe to the most general event you care about, and branch inside the listener with `instanceof` if you need to tell them apart.
## Events you can subscribe to
| Module | Events | Payload |
| - | - | - |
| User | `UserCreatedEvent`, `UserUpdatedEvent`, `UserDeletedEvent`, `EmailVerifiedEvent`, `UserEmailUpdatedEvent`, `UserPasswordUpdatedEvent`, `PasswordRecoveryCreatedEvent` | `$event->user` |
| Workspace | `WorkspaceCreatedEvent`, `WorkspaceUpdatedEvent`, `WorkspaceDeletedEvent`, `InvitationCreatedEvent` | `$event->workspace`, or the invitation |
| Billing | `OrderCreatedEvent`, `OrderFulfilledEvent` | `$event->order` |
| Billing | `SubscriptionCreatedEvent`, `SubscriptionActivatedEvent`, `SubscriptionUsageResetEvent` | `$event->subscription` |
| Billing | `PlanCreatedEvent`, `PlanUpdatedEvent`, `PlanDeletedEvent`, `CouponCreatedEvent`, `CouponUpdatedEvent`, `CouponDeletedEvent` | The plan or coupon |
| Billing | `CreditUsageEvent` | `$event->workspace`, `$event->count` |
| Option | `OptionCreatedEvent`, `OptionUpdatedEvent`, `OptionDeletedEvent` | The option |
| Preset | `PresetCreatedEvent`, `PresetUpdatedEvent`, `PresetDeletedEvent` | The preset |
| Category | `CategoryCreatedEvent`, `CategoryUpdatedEvent`, `CategoryDeletedEvent` | The category |
| Cron | `CronEvent` | none |
Class names are under `\Domain\Events\`. See the [event catalog](/development/core/events) for the full list, including who listens in core.
## Dispatch your own events
```php theme={null}
use Psr\EventDispatcher\EventDispatcherInterface;
public function __construct(
private EventDispatcherInterface $dispatcher,
) {}
$this->dispatcher->dispatch(new NoteCreatedEvent($note));
```
Other plugins can then subscribe to your event, which is how you make your plugin extensible.
## Scheduled work
There's no queue. Recurring work runs when `cron.php` executes, which dispatches `Cron\Domain\Events\CronEvent`. Subscribe to it like any other event.
```php theme={null}
$this->mapper->addEventListener(CronEvent::class, SyncContacts::class);
```
A cron listener should process a bounded batch and remember where it stopped, because the next tick may be a minute away:
```php src/Listeners/SyncContacts.php theme={null}
sync ?? [];
// Only run while an administrator has started a sync.
if (($state['status'] ?? null) !== 'processing') {
return;
}
$cmd = new ListUsersCommand();
$cmd->setLimit(self::BATCH_SIZE);
if (!empty($state['cursor_id'])) {
$cmd->setCursor($state['cursor_id'], 'starting_after');
}
$synced = (int) ($state['synced'] ?? 0);
$failed = (int) ($state['failed'] ?? 0);
$cursor = $state['cursor_id'] ?? null;
$processed = 0;
foreach ($this->dispatcher->dispatch($cmd) as $user) {
$processed++;
$cursor = (string) $user->getId()->getValue();
try {
$this->client->upsertContact([
'email' => (string) $user->getEmail()->value,
]);
$synced++;
} catch (Throwable) {
$failed++;
}
}
$this->dispatcher->dispatch(new SaveOptionCommand('crm', json_encode([
'sync' => [
'status' => $processed === 0 ? 'completed' : 'processing',
'cursor_id' => $cursor,
'synced' => $synced,
'failed' => $failed,
],
])));
}
}
```
Points to copy from this pattern:
* **Opt in.** Do nothing unless an administrator started the job, so an idle installation pays nothing for your plugin.
* **Batch.** Process a fixed number of records per tick.
* **Persist a cursor.** Store the last processed ID in an option, so the next tick continues instead of restarting.
* **Track counters and a status.** `processing`, `paused` and `completed` give the admin page something to show, and stopping is just a status change.
* **Rate-limit yourself.** For a daily job, store a timestamp and return early until it's due.
Entity changes made in cron listeners are flushed after the event finishes, the same as in a web request.
## Testing
* Run the cron tick by hand with `php cron.php`.
* Trigger domain events by doing the thing in the UI, such as creating a user.
* Watch `var/log/app-*.log`, and log at the start and end of your listener while developing.
## Related
* [Core event catalog](/development/core/events)
* [Background jobs and cron](/development/core/background-jobs-and-cron)
* [Event-driven integrations guide](/development/plugins/guides/event-driven-integration)
* [Data and persistence](/development/plugins/data-and-persistence)
# Files and storage
Source: https://docs.aikeedo.com/development/plugins/files-and-storage
Store and serve files from an Aikeedo plugin using the CDN abstraction, the local filesystem, and file entities.
Aikeedo has two filesystem services. Use the CDN for anything a user will see, and the local filesystem for private working files. Both are Flysystem instances, so the usual `write()`, `read()`, `delete()` and `listContents()` methods are available.
| Service | Backed by | Use for |
| - | - | - |
| `Shared\Infrastructure\FileSystem\CdnInterface` | The storage adapter the administrator selected: local, S3-compatible, and so on | Generated images, uploads, anything served to users |
| `Shared\Infrastructure\FileSystem\FileSystemInterface` | The local installation directory | Temporary files, imports, logs, scratch space under `var/` |
## Store a user-visible file
```php src/Services/AttachmentStore.php theme={null}
getStream();
// Let the CDN decide the path; it applies the configured grouping.
$key = $this->cdn->generatePath('pdf', $workspace, $user);
$this->cdn->write($key, $contents, [
'visibility' => Visibility::PUBLIC,
]);
return new FileEntity(
new Storage($this->cdn->getAdapterLookupKey()),
new ObjectKey($key),
new Url((string) $this->cdn->getUrl($key)),
new Size($file->getSize() ?? strlen($contents)),
);
}
}
```
| Method | Purpose |
| - | - |
| `generatePath(string $ext, ?WorkspaceEntity, ?UserEntity)` | Builds a unique object key with the configured grouping, such as per workspace or per user |
| `write(string $path, string $contents, array $config)` | Writes the file; pass `['visibility' => Visibility::PUBLIC]` for files served directly |
| `getUrl(string $path)` | Public or signed URL, depending on the adapter and whether URL signing is enabled |
| `getAdapterLookupKey()` | The active adapter's key, which you store on the file entity so it can be resolved later |
| `setVisibility(string $path, string $visibility)` | Changes visibility after the fact |
| `delete(string $path)` | Removes the object |
Don't build URLs yourself. When signed URLs are enabled, `getUrl()` returns a time-limited URL, and a hardcoded path won't work.
### File entities
`File\Domain\Entities\FileEntity` and its image subclass record where a file lives so other code can resolve it later. Images take dimensions and a blurhash placeholder:
```php theme={null}
use File\Domain\Entities\ImageFileEntity;
use File\Domain\ValueObjects\Height;
use File\Domain\ValueObjects\Width;
use File\Infrastructure\BlurhashGenerator;
$image = imagecreatefromstring($contents);
$width = imagesx($image);
$height = imagesy($image);
$entity = new ImageFileEntity(
new Storage($this->cdn->getAdapterLookupKey()),
new ObjectKey($key),
new Url((string) $this->cdn->getUrl($key)),
new Size(strlen($contents)),
new Width($width),
new Height($height),
BlurhashGenerator::generateBlurHash($image, $width, $height),
);
$entity->addMeta('name', $file->getClientFilename());
$entity->addMeta('mime', 'image/png');
```
## Private working files
```php theme={null}
use Shared\Infrastructure\FileSystem\FileSystemInterface;
public function __construct(
private FileSystemInterface $fs,
) {}
$this->fs->write('var/imports/' . $id . '.zip', $contents);
$stream = $this->fs->readStream('var/imports/' . $id . '.zip');
$this->fs->delete('var/imports/' . $id . '.zip');
```
Paths are relative to the installation root. Keep scratch files under `var/`, which isn't web-accessible, and clean them up when you're done.
## Validate uploads
Never trust the client. At minimum, cap the size and sniff the real MIME type:
```php theme={null}
use Presentation\Exceptions\HttpException;
use Easy\Http\Message\StatusCode;
private const MAX_BYTES = 5 * 1024 * 1024;
private const ALLOWED = ['image/png', 'image/jpeg', 'application/pdf'];
$file = $request->getUploadedFiles()['file'] ?? null;
if (!$file) {
throw new HttpException('No file uploaded', StatusCode::BAD_REQUEST);
}
if (($file->getSize() ?? 0) > self::MAX_BYTES) {
throw new HttpException('File is too large', StatusCode::BAD_REQUEST);
}
$contents = (string) $file->getStream();
$mime = (new \finfo(FILEINFO_MIME_TYPE))->buffer($contents);
if (!in_array($mime, self::ALLOWED, true)) {
throw new HttpException('Unsupported file type', StatusCode::BAD_REQUEST);
}
```
Decide the extension from the sniffed MIME type, not from the client's filename, and never write a file whose key comes from user input. `generatePath()` avoids both problems.
Base64 payloads in a JSON body work the same way once decoded: check the decoded length before you write anything.
## Adding a storage backend
Everything above uses whichever adapter the administrator selected. To add a new backend, such as another S3-compatible provider, implement the CDN adapter interface and register it. See [Storage adapters](/development/plugins/guides/storage-adapter).
## Related
* [Data and persistence](/development/plugins/data-and-persistence)
* [Storage adapters guide](/development/plugins/guides/storage-adapter)
* [Storage internals](/development/core/storage-and-files)
* [Cloud storage setup](/integrations/cloud-storage/overview)
# Frontend integration
Source: https://docs.aikeedo.com/development/plugins/frontend-integration
Add JavaScript and CSS to plugin pages, reuse Aikeedo's Alpine components, web components and API client, and ship your own built bundles.
Aikeedo pages are server-rendered Twig with Alpine.js for interactivity. A plugin page gets the same building blocks: the layout's blocks, the global API client, and the web components the core registers.
## Add scripts and styles to a page
The base layout exposes `styles` and `scripts` blocks. Call `{{ parent() }}` so the core's own assets keep loading:
```twig templates/notes.twig theme={null}
{% extends "/layouts/main.twig" %}
{% set xdata = 'notes' %}
{% block styles %}
{{ parent() }}
{% endblock %}
{% block scripts %}
{{ parent() }}
{% endblock %}
{% block template %}
{{ p__('heading', 'Notes') }}
{% endblock %}
```
Including a `.js` or `.css` file through your Twig namespace inlines it, which keeps a small plugin to two files and no build step. For anything larger, ship a built bundle instead.
## Write the Alpine component
`xdata` names the component the page binds to. Register it with `Alpine.data()` before Alpine starts:
```javascript assets/notes.js theme={null}
document.addEventListener('alpine:init', () => {
Alpine.data('notes', () => ({
notes: [],
isProcessing: false,
init() {
this.fetch();
},
fetch() {
window.api.get('/notes')
.then(res => this.notes = res.data.data)
.catch(() => window.toast.error('Could not load notes'));
},
create() {
if (this.isProcessing) {
return;
}
this.isProcessing = true;
window.api.post('/notes', { title: 'Untitled' })
.then(res => {
this.notes.unshift(res.data);
window.toast.show('Note created');
})
.finally(() => this.isProcessing = false);
},
}));
});
```
### Pass server data to the component
Render data as JSON and read it from a ref, instead of interpolating values into JavaScript:
```twig theme={null}
```
```javascript theme={null}
init() {
const config = JSON.parse(this.$refs.config.textContent);
}
```
## Globals you can use
| Global | What it does |
| - | - |
| `window.api` | A configured HTTP client. On app pages its base URL is `/api`; on admin pages it's `/admin/api`. It attaches the session token and workspace header for you. |
| `window.modal` | Opens and closes modal dialogs by ID: `window.modal.open('delete-note-modal')` |
| `window.toast` | Shows notifications: `toast.show()`, `toast.success()`, `toast.error()` |
Because `window.api` already points at the right base, call it with a path relative to that base: `window.api.get('/notes')` hits `/api/notes` from an app page.
The client resolves with the `fetch` response, plus a `data` property holding the parsed JSON body, which is why the examples read `res.data`. It also handles two cases for you: a failed request shows the server's error message as a toast and rejects with an `ApiError`, and a `401` sends the user back to the login page.
## Reuse the core's web components
These custom elements are registered by the core bundles, so plugin templates can use them directly:
| Element | Purpose |
| - | - |
| `` | Wraps a form, manages submit state and validation styling |
| `` | Avatar or icon tile, also used for navigation icons |
| `` | Copy-to-clipboard control |
| `` | Formats a minor-unit amount in a currency |
| `` | Formats a credit balance, including "unlimited" |
| `` | Formats a timestamp |
| `` | Formats a byte count |
| `` | File drop target |
| `` | Syntax-highlighted code block |
| `` | Renders Markdown |
| `` | Chart rendering |
| `` | A modal dialog, opened through `window.modal` |
```twig theme={null}
{{ webhook_url }}
```
Core snippets are includable too, such as `{% include "/snippets/spinner.twig" %}`.
Match the surrounding markup: `box`, `button`, `button-accent`, `input` and `label` are the core's Tailwind component classes, so your page inherits the admin and app styling, including dark mode.
## Ship a built bundle
Inlining stops making sense once you have a real frontend, for example a widget or an editor. Build with Vite, publish the output, and read the manifest from PHP.
```javascript vite.config.js theme={null}
import { defineConfig } from 'vite';
export default defineConfig({
build: {
outDir: 'public/assets',
manifest: true,
rollupOptions: {
input: { app: 'assets/src/index.js' },
},
},
});
```
```json composer.json theme={null}
{
"extra": {
"public": [
{ "source": "public/assets/*", "target": "." }
]
}
}
```
The installer copies those files to `public/e/{vendor}/{name}/`, so they're served from `/e/acme/notes/...`. Read the manifest to resolve the hashed filenames:
```php theme={null}
$manifestPath = dirname(__DIR__) . '/public/assets/.vite/manifest.json';
$manifest = json_decode((string) file_get_contents($manifestPath), true);
$entry = $manifest['assets/src/index.js']['file'] ?? null;
$url = '/e/acme/notes/' . $entry;
```
For local development, let an environment variable point at your dev server and fall back to the published files:
```twig theme={null}
{% set base = env.ACME_NOTES_ASSETS_SERVER|default('/e/acme/notes') %}
```
See [Public assets](/development/plugins/public-assets) for the copy rules.
## Related
* [App pages and APIs](/development/plugins/app-pages-and-apis)
* [Admin settings pages](/development/plugins/admin-settings-pages)
* [Public assets](/development/plugins/public-assets)
* [Views and frontend internals](/development/core/views-and-frontend)
# Add an AI provider
Source: https://docs.aikeedo.com/development/plugins/guides/ai-provider
Implement an AI capability interface in an Aikeedo plugin, register it with the service factory, and make your models selectable.
Aikeedo resolves AI work through a factory: it asks for a capability interface and a model, and gets back the first registered service that supports that model. A plugin registers its own services the same way the core does.
## Decide what to build
Your provider has its own API. Implement the capability interfaces you support, register them, and add your models to the registry.
The provider speaks the OpenAI API. No plugin is needed: administrators add the server under **Settings → AI models** and import its models. Point your users there instead of writing code.
## The contracts
Every service implements `Ai\Domain\Services\AiServiceInterface`:
```php theme={null}
interface AiServiceInterface
{
public function supportsModel(Model $model): bool;
public function getSupportedModels(): Traversable;
}
```
Then add the capability interfaces you implement:
| Interface | Method |
| - | - |
| `Ai\Domain\Completion\MessageServiceInterface` | `generateMessage(Model, MessageEntity, ?array $params): Generator` |
| `Ai\Domain\Completion\TextCompletionServiceInterface` | `complete(Model, string $instructions, string $input, int $maxTokens = 512): TextCompletionResponse` |
| `Ai\Domain\Image\ImageServiceInterface` | `generateImage(WorkspaceEntity, UserEntity, Model, ?array $params): ImageEntity` |
| `Ai\Domain\Video\VideoServiceInterface` | `generateVideo(WorkspaceEntity, UserEntity, Model, ?array $params): VideoEntity` |
| `Ai\Domain\Speech\SpeechServiceInterface` | `getVoiceList()`, `generateSpeech(VoiceEntity, array $params)` |
| `Ai\Domain\Transcription\TranscriptionServiceInterface` | `generateTranscription(Model, StreamInterface, array $params)` |
| `Ai\Domain\Classification\ClassificationServiceInterface` | `generateClassification(Model, string $input)` |
| `Ai\Domain\Embedding\EmbeddingServiceInterface` | `generateEmbedding(Model, string $text)` |
| `Ai\Domain\IsolatedVoice\VoiceIsolatorServiceInterface` | `generateIsolatedVoice(Model, StreamInterface, array $params)` |
Implement only what your provider actually does. A provider can be chat-only, or image-only.
## A text completion service
```php src/Services/AcmeTextCompletionService.php theme={null}
value, self::MODELS, true);
}
#[Override]
public function getSupportedModels(): Traversable
{
foreach (self::MODELS as $model) {
yield new Model($model);
}
}
#[Override]
public function complete(
Model $model,
string $instructions,
string $input,
int $maxTokens = 512
): TextCompletionResponse {
try {
$resp = $this->client->post('/v1/completions', [
'model' => $model->value,
'system' => $instructions,
'prompt' => $input,
'max_tokens' => $maxTokens,
]);
} catch (Throwable $th) {
throw new ApiException($th->getMessage(), previous: $th);
}
$cost = $this->calculator->calculate(
$resp->usage->input_tokens,
$model,
CostCalculator::INPUT
);
return new TextCompletionResponse(trim($resp->text), $cost);
}
}
```
Throw `Ai\Domain\Exceptions\ApiException` when the provider fails. Aikeedo surfaces it to the user instead of a generic error.
## A streaming chat service
`MessageServiceInterface::generateMessage()` returns a generator of stream parts. Yield deltas as they arrive, and finish with a usage part carrying the cumulative cost:
```php theme={null}
use Ai\Domain\ValueObjects\Stream\TextDeltaPart;
use Ai\Domain\ValueObjects\Stream\UsagePart;
public function generateMessage(
Model $model,
MessageEntity $message,
?array $params = null
): Generator {
$stream = $this->client->stream('/v1/chat', [
'model' => $model->value,
'messages' => $this->history->build($message),
]);
$inputTokens = 0;
$outputTokens = 0;
foreach ($stream as $event) {
if ($event->type === 'delta') {
$outputTokens += 1;
yield new TextDeltaPart($event->text);
}
if ($event->type === 'usage') {
$inputTokens = $event->input_tokens;
$outputTokens = $event->output_tokens;
}
}
$cost = new CreditCount(
$this->calculator->calculate($inputTokens, $model, CostCalculator::INPUT)->value
+ $this->calculator->calculate($outputTokens, $model, CostCalculator::OUTPUT)->value
);
// Cumulative total; the handler overwrites its tracked cost with the last one.
yield new UsagePart($cost);
}
```
## Register the services
```php src/Plugin.php theme={null}
use Acme\Provider\Services\AcmeMessageService;
use Acme\Provider\Services\AcmeTextCompletionService;
use Ai\Infrastructure\Services\AiServiceFactory;
public function __construct(
private AiServiceFactory $factory,
) {}
public function boot(Context $context): void
{
$this->factory
->register(AcmeMessageService::class)
->register(AcmeTextCompletionService::class);
}
```
Registration is lazy: the class is only built when a model needs it. The factory returns the **first** registered service that implements the requested interface and supports the model, so keep `supportsModel()` strict.
## Make your models selectable
Services answer "can I handle this model". What users pick from comes from the model registry, which is `config/registry/base.json` merged with the installation's own `config/registry/registry.json`.
A plugin adds its models by writing to that registry, which is exactly how custom servers are stored. Do it in the install or activate hook, not on every boot:
```php src/Plugin.php theme={null}
use Plugin\Domain\Hooks\ActivateHookInterface;
use Shared\Infrastructure\Services\ModelRegistry;
public function __construct(
private ModelRegistry $registry,
) {}
public function activate(Context $context): void
{
$directory = $this->registry['directory'];
foreach ($directory as $service) {
if (($service['key'] ?? null) === 'acme') {
return; // already registered
}
}
$directory[] = [
'key' => 'acme',
'name' => 'Acme AI',
// Required for entries that aren't in base.json, or the merge drops them.
'custom' => true,
'models' => [
[
'type' => 'chat',
'key' => 'acme-large',
'name' => 'Acme Large',
'description' => 'General purpose chat model',
'custom' => true,
'enabled' => true,
'multiplier' => 1000,
'modalities' => ['input' => ['text'], 'output' => ['text']],
'rates' => [
['key' => 'acme-large-input', 'type' => 'input', 'unit' => 'token'],
['key' => 'acme-large-output', 'type' => 'output', 'unit' => 'token'],
],
],
],
];
$this->registry['directory'] = $directory;
$this->registry->save();
}
```
Entries that don't exist in `base.json` survive the merge only when they're marked `"custom": true`. Without that flag your service and models disappear on the next boot.
Key fields:
| Field | Meaning |
| - | - |
| `key` | The model identifier your `supportsModel()` matches |
| `type` | The capability, such as `chat`, `image`, `video`, `speech`, `transcription`, `embedding` |
| `multiplier` | Used by `CostCalculator::estimate()` as a pre-flight cost |
| `rates` | Rate keys administrators price under **Settings → Credit rates** |
| `modalities`, `specs`, `config` | What the model accepts, and what the UI offers for it |
Remove your entries again in the uninstall hook, so the registry doesn't keep models nothing can serve.
## Credits
Aikeedo charges the workspace based on what your service reports. Use `CostCalculator` with the rate keys you declared, and return the cost on the response or as a `UsagePart`. See [AI models and credits](/development/plugins/ai-models-and-credits).
## Testing
Your models appear under **Settings → AI models** and can be enabled.
A plan that grants the model lets a user select it in chat.
Streaming shows text arriving progressively, not all at once at the end.
Credits are deducted, and the amount matches the configured rates.
A provider error shows a readable message instead of a `500`.
Deactivating the plugin leaves the installation working, with your models simply unavailable.
## Related
* [AI models and credits](/development/plugins/ai-models-and-credits)
* [AI subsystem internals](/development/core/ai-subsystem)
* [Custom extension points](/development/plugins/custom-extension-points)
* [OpenAI-compatible servers](/integrations/openai-api-compatibility)
# Add an AI tool
Source: https://docs.aikeedo.com/development/plugins/guides/ai-tool
Implement a chat tool in an Aikeedo plugin, understand how the core decides which tools a model may call, and know the current limits.
Tools are what a chat model can call while answering: search the web, read a page, generate an image, look something up in a knowledge base. A tool is a small class with a JSON schema and a `call()` method.
Read [what the core will and won't offer](#what-the-core-offers-to-models) before you build. A brand-new tool key can be registered, but the built-in chat flow only offers tools whose key appears in the plan configuration, and that list is fixed. Your options are to replace an existing tool's implementation, or to call your tool from your own plugin's flows.
## The interface
```php theme={null}
namespace Ai\Infrastructure\Services\Tools;
interface ToolInterface
{
public function isEnabled(): bool;
public function getDescription(): string;
public function getDefinitions(): array;
public function call(
ConversationEntity $conversation,
WorkspaceEntity $workspace,
UserEntity $user,
?AssistantEntity $assistant = null,
?array $params = null,
): CallResponse;
public function getSystemInstructions(?MessageEntity $message = null): ?string;
}
```
| Method | Purpose |
| - | - |
| `isEnabled()` | Whether the tool is configured and switched on |
| `getDescription()` | What the tool does, sent to the model so it knows when to call it |
| `getDefinitions()` | JSON Schema for the arguments |
| `call()` | Runs the tool and returns its result and cost |
| `getSystemInstructions()` | Extra system prompt text, or `null` |
## Implementation
```php src/Tools/WeatherTool.php theme={null}
apiKey;
}
#[Override]
public function getDescription(): string
{
return 'Look up the current weather for a city.';
}
#[Override]
public function getDefinitions(): array
{
return [
'type' => 'object',
'properties' => [
'city' => [
'type' => 'string',
'description' => 'City name, optionally with a country code.',
],
],
'required' => ['city'],
];
}
#[Override]
public function getSystemInstructions(?MessageEntity $message = null): ?string
{
return 'Use the weather tool whenever the user asks about current conditions.';
}
#[Override]
public function call(
ConversationEntity $conversation,
WorkspaceEntity $workspace,
UserEntity $user,
?AssistantEntity $assistant = null,
?array $params = null,
): CallResponse {
$city = $params['city'] ?? null;
if (!$city) {
throw new CallException('No city was provided.');
}
try {
$weather = $this->client->current($city);
} catch (Throwable $th) {
throw new CallException('Weather lookup failed.', previous: $th);
}
// Return text the model can read, plus what the call cost.
return new CallResponse(
content: sprintf('%s: %s, %d°C', $city, $weather->summary, $weather->temp),
cost: new CreditCount(0),
);
}
}
```
`CallResponse` takes the text result, the cost in credits, and optionally a library item when your tool produced something like an image.
### Writing a good tool
* **Describe it for a model, not a human.** Say when to use it and what it returns.
* **Keep the schema small.** Few parameters, clear names, explicit `required`.
* **Return text.** The content goes back into the conversation, so make it readable and compact.
* **Be fast, and time out.** A slow tool stalls the whole answer.
* **Throw `CallException`** for failures, so the model sees an error instead of a broken stream.
* **Charge for expensive work** with `CostCalculator`, and return the amount in the response.
## Register the tool
```php src/Plugin.php theme={null}
use Acme\Weather\Tools\WeatherTool;
use Ai\Infrastructure\Services\Tools\ToolCollection;
public function __construct(
private ToolCollection $tools,
) {}
public function boot(Context $context): void
{
$this->tools->add(WeatherTool::LOOKUP_KEY, WeatherTool::class);
}
```
## What the core offers to models
When a message is generated, the tool collection decides which tools to expose. It checks, in order:
1. `isEnabled()` on the tool.
2. Whether the conversation is temporary, which disables context tools such as memory and canvas.
3. **Whether the plan's tool configuration has the tool's key enabled.**
4. Whether the user disabled that capability in their preferences.
Step 3 is the limit: `Billing\Domain\ValueObjects\PlanConfig` builds its `tools` map from a fixed list of keys.
| Key | Capability |
| - | - |
| `embedding_search` | Search uploaded files |
| `google_search` | Web search |
| `youtube` | YouTube lookups |
| `web_scrap` | Fetch a web page |
| `generate_media` | Images, video, speech and transcription |
| `memory` | Memories and chat history |
| `canvas` | Canvas documents |
| `ask_questions` | Interactive follow-up questions |
A key outside that list is never enabled on a plan, so the collection never yields it to the model.
### What you can do instead
Register your class under an existing key, such as `google_search`, to swap the provider behind a capability administrators already control. Keep the same argument schema so existing prompts keep working.
If your plugin runs its own AI flow, for example an assistant of your own, you own the tool loop: build the definitions, call your tool when the model asks for it, and feed the result back. Nothing restricts which tools you offer there.
Some "tools" are better as context. Fetch what's needed and add it to the system instructions rather than waiting for the model to ask.
## Testing
With the capability enabled on the plan, the model calls your tool when it should.
The tool's result appears in the answer, and the tool call is shown in the transcript.
A failure produces a graceful message rather than a broken response.
Credits are charged when the tool costs money.
## Related
* [AI models and credits](/development/plugins/ai-models-and-credits)
* [AI providers](/development/plugins/guides/ai-provider)
* [AI subsystem internals](/development/core/ai-subsystem)
# Build a currency rate provider
Source: https://docs.aikeedo.com/development/plugins/guides/currency-rate-provider
Supply exchange rates to Aikeedo so payment gateways can charge in a currency other than the platform default.
Aikeedo prices plans in one currency. When a payment gateway bills in a different one, it converts the amount through the configured rate provider. Without a provider, no conversion happens and everything is charged in the default currency.
## The interface
```php theme={null}
namespace Billing\Infrastructure\Currency;
interface RateProviderInterface
{
public function getName(): string;
public function getRate(CurrencyCode $from, CurrencyCode $to): int|float;
}
```
`getRate()` returns the multiplier that converts an amount in `$from` into `$to`. It's called during checkout, so it must be fast and must not throw for a currency pair you support.
## Implementation
Rates change slowly, so fetch them in bulk and cache them. Storing them in an option makes them survive cache clears and keeps the checkout path free of network calls.
```php src/AcmeRates.php theme={null}
|null */
#[Inject('option.acme_rates.rates')]
private ?array $rates = null,
) {}
#[Override]
public function getName(): string
{
return 'Acme Rates';
}
#[Override]
public function getRate(CurrencyCode $from, CurrencyCode $to): int|float
{
$rates = $this->rates();
if (!isset($rates[$from->value], $rates[$to->value])) {
throw new RuntimeException(sprintf(
'No rate for %s to %s',
$from->value,
$to->value
));
}
// Rates are quoted against a single base currency.
return $rates[$to->value] / $rates[$from->value];
}
/** @return array */
private function rates(): array
{
$now = time();
if ($this->rates && $this->updatedAt && $this->updatedAt + self::TTL >= $now) {
return $this->rates;
}
if (!$this->apiKey) {
throw new RuntimeException('Acme Rates API key is not configured');
}
$rates = $this->client->fetchRates($this->apiKey);
$this->dispatcher->dispatch(new SaveOptionCommand(
'acme_rates',
json_encode(['updated_at' => $now, 'rates' => $rates])
));
$this->rates = $rates;
return $rates;
}
}
```
A conversion failure is swallowed by the billing helper, which then charges the original amount in the original currency. That's safer than blocking checkout, but it means a broken provider is invisible to the customer. Log failures so administrators can see them.
## Register it
```php src/Plugin.php theme={null}
use Billing\Infrastructure\Currency\RateProviderCollectionInterface;
public function __construct(
private RateProviderCollectionInterface $providers,
private FilesystemLoader $loader,
private AttributeMapper $mapper,
) {}
public function boot(Context $context): void
{
$this->loader->addPath(__DIR__ . '/../templates', 'acme-rates');
$this->mapper->addPath(__DIR__);
$this->providers->add(AcmeRates::LOOKUP_KEY, AcmeRates::class);
}
```
The provider appears in the **Currency rate provider** select under **Settings → Billing**, and the choice is stored in `option.currency.provider`.
## Settings page
The billing page links to `/admin/settings/rate-providers/{key}`, so declare that route and point `extra.default_url` at it:
```php theme={null}
#[Route(path: '/settings/rate-providers/acme-rates', method: RequestMethod::GET)]
class SettingsRequestHandler extends AbstractAdminViewRequestHandler implements
RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new ViewResponse('@acme-rates/settings.twig');
}
}
```
Show the API key field, and the timestamp of the last successful refresh, so administrators can tell whether rates are current.
## Testing
Set a gateway to a currency other than the platform default and confirm the charge is converted.
The converted amount is plausible, and rounding lands on whole minor units.
Rates are fetched once and reused until they expire.
With an invalid API key, checkout still completes in the default currency.
## A public reference implementation
The [Currency Beacon plugin](https://github.com/heyaikeedo/plugins-currency-beacon) is a small, complete rate-provider plugin you can read end to end.
## Related
* [Payment gateways](/development/plugins/guides/payments/overview)
* [Billing internals](/development/core/billing-subsystem)
* [Billing settings](/billing/overview)
# Build an embeddable widget
Source: https://docs.aikeedo.com/development/plugins/guides/embeddable-widget
Ship a plugin whose widget runs on third-party websites: a loader script, an iframe app, visitor tokens, streamed AI replies and workspace billing.
This guide combines the public-endpoint, asset, streaming and billing building blocks into one feature: `acme/feedback-widget`, a widget a customer embeds on their own site, where visitors ask questions and get AI-generated answers.
It's the most security-sensitive kind of plugin you can build, so read [Public pages and endpoints](/development/plugins/public-endpoints-and-embeds) first.
## Architecture
```mermaid theme={null}
flowchart TD
A[Customer's website] -->|script tag| B[loader.js from your web root]
B --> C["iframe to /feedback/{id}/embed"]
C --> D[Widget app bundle]
D -->|"POST /feedback/{id}/session"| E[Session endpoint]
E -->|visitor JWT| D
D -->|"POST /feedback/{id}/messages"| F[Message endpoint]
F --> G[AI service + workspace credits]
G -->|server-sent events| D
```
| Piece | Where it lives |
| - | - |
| Loader script | Published to the web root through `extra.public` |
| Embed page | A public route rendering a standalone HTML page |
| Widget app | A built bundle published under `/e/acme/feedback-widget/` |
| API | Public routes with your own middleware stack |
| Configuration | A widget record per workspace, with a secret and trusted domains |
## 1. Publish the loader
```json composer.json theme={null}
{
"require": { "heyaikeedo/composer": "^1.2.0" },
"extra": {
"public": [
{ "source": "widget/dist/loader.js", "target": "/acme-feedback.js" },
{ "source": "widget/dist/app/*", "target": "." }
]
}
}
```
The loader ends up at `/acme-feedback.js`, and the app bundle under `/e/acme/feedback-widget/`. The customer embeds one tag:
```html theme={null}
```
The loader creates an iframe pointing at your embed route, and communicates with it over `postMessage`:
```javascript widget/src/loader.js theme={null}
const script = document.currentScript;
const id = script.dataset.id;
const base = new URL(script.src).origin;
const frame = document.createElement('iframe');
frame.src = `${base}/feedback/${id}/embed`;
frame.style.cssText = 'position:fixed;bottom:16px;right:16px;border:0;width:380px;height:560px;';
document.body.appendChild(frame);
window.addEventListener('message', (event) => {
// Only trust messages from the iframe you created.
if (event.origin !== base) return;
if (event.data?.type === 'acme-feedback:resize') {
frame.style.height = `${event.data.height}px`;
}
});
```
## 2. Serve the embed page
```php src/RequestHandlers/EmbedView.php theme={null}
#[Middleware(ExceptionMiddleware::class)]
#[Route(path: '/feedback/[uuid:wid]/embed', method: RequestMethod::GET)]
class EmbedView implements RequestHandlerInterface
{
public function __construct(
private Dispatcher $dispatcher,
private Environment $twig,
#[Inject('option.features.feedback.is_enabled')]
private ?bool $isEnabled = false,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
if (!$this->isEnabled) {
throw new NotFoundException();
}
$widget = $this->dispatcher->dispatch(
new ReadWidgetCommand($request->getAttribute('wid'))
);
if (!$widget->isActive()) {
throw new NotFoundException();
}
$html = $this->twig->render('@acme-feedback/embed.twig', [
'widget' => new WidgetResource($widget),
]);
$domains = $widget->getTrustedDomains();
return (new HtmlResponse($html))->withHeader(
'Content-Security-Policy',
'frame-ancestors ' . ($domains ? implode(' ', $domains) : "'none'")
);
}
}
```
`frame-ancestors` is the control that actually stops the widget being framed elsewhere. Default to `'none'` when no domains are configured, rather than allowing everything.
The template is a standalone page, not the app layout: ``, your bundle, and a JSON config block.
## 3. Authenticate visitors
A visitor isn't an Aikeedo user. Issue a short-lived token, signed with the widget's own secret:
```php src/RequestHandlers/Api/SessionRequestHandler.php theme={null}
#[Route(path: 'session', method: RequestMethod::POST)]
class SessionRequestHandler extends AbstractWidgetApi implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$widget = $request->getAttribute(WidgetEntity::class);
// Always create a fresh visitor. Never accept an id from the caller.
$visitor = $this->dispatcher->dispatch(new CreateVisitorCommand($widget));
return new JsonResponse([
'token' => $this->tokens->issue(
(string) $widget->getId()->getValue(),
(string) $visitor->getId()->getValue(),
(string) $widget->getSecret()->value,
),
]);
}
}
```
If you support identity verification, where the customer's site tells you who the visitor is, require the claim to be signed with the widget secret on the customer's server. Accepting an unsigned identifier lets anyone read another visitor's conversation.
Your middleware then authenticates later requests:
```php src/Middlewares/VisitorMiddleware.php theme={null}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$widget = $request->getAttribute(WidgetEntity::class);
$header = $request->getHeaderLine('Authorization');
if (!preg_match('/Bearer\s(\S+)/', $header, $matches)) {
throw new UnauthorizedException();
}
$claims = $this->tokens->parse($matches[1], (string) $widget->getSecret()->value);
// The token must belong to this widget.
if (($claims->wid ?? null) !== (string) $widget->getId()->getValue()) {
throw new UnauthorizedException();
}
$visitor = $this->visitors->ofId(new Id($claims->sub));
return $handler->handle($request->withAttribute(VisitorEntity::class, $visitor));
}
```
## 4. Answer with AI, billed to the owner
The visitor pays nothing; the workspace that owns the widget does.
```php theme={null}
$workspace = $widget->getWorkspace();
$model = new Model($widget->getModel()->value);
if (!$workspace->isModelGranted($model)) {
throw new HttpException('This widget is unavailable', StatusCode::SERVICE_UNAVAILABLE);
}
// Throws when the workspace is out of credits.
$this->billing->assertCanConsume($workspace, $model);
$service = $this->factory->create(MessageServiceInterface::class, $model);
```
Stream the answer back as server-sent events, and consume the real cost when the stream finishes:
```php theme={null}
$response = (new Response())
->withHeader('Content-Type', 'text/event-stream')
->withHeader('Cache-Control', 'no-cache')
->withHeader('X-Accel-Buffering', 'no');
return $response->withBody(new CallbackStream($this->stream(...), $generator, $workspace, $model));
```
See [Streaming responses](/development/plugins/streaming-responses) and [AI models and credits](/development/plugins/ai-models-and-credits) for the details.
## 5. Verify ownership everywhere
Every route below `/feedback/{wid}/` takes an ID from an untrusted caller. Check the whole chain:
```php theme={null}
$widget = $request->getAttribute(WidgetEntity::class);
$visitor = $request->getAttribute(VisitorEntity::class);
$thread = $this->threads->ofId(new Id($request->getAttribute('tid')));
// The thread must belong to this visitor AND this widget.
if (
!$thread->getVisitor()->getId()->equals($visitor->getId())
|| !$thread->getWidget()->getId()->equals($widget->getId())
) {
throw new NotFoundException();
}
```
Checking only the parent, or only the token, is the single most common way a widget leaks one customer's conversations to another.
## 6. Development
Point an environment variable at your bundler while you work:
```twig embed.twig theme={null}
{% set base = env.ACME_FEEDBACK_ASSETS_SERVER|default('/e/acme/feedback-widget') %}
```
Test the embed on a separate local origin, not inside the Aikeedo tab, so you exercise the real cross-origin path.
## Launch checklist
`frame-ancestors` is set, and defaults to `'none'` when nothing is configured.
Tokens are signed per widget, short-lived, and never minted from caller-supplied IDs.
Every nested ID is checked against both the visitor and the widget.
Credits are billed to the widget's workspace, and checked before generation starts.
Requests are rate-limited per visitor and per widget, and payload size is capped.
Responses contain nothing internal: no workspace details, no configuration, no other visitors.
Disabling the feature or deactivating the widget returns `404` for every route.
The loader is small, framework-free, and degrades quietly if the iframe fails to load.
## Official chatbots
If you need embeddable AI chatbots rather than a custom widget, the official Chatbots plugin is available on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Related
* [Public pages and endpoints](/development/plugins/public-endpoints-and-embeds)
* [Streaming responses](/development/plugins/streaming-responses)
* [AI models and credits](/development/plugins/ai-models-and-credits)
* [Public assets](/development/plugins/public-assets)
# Build an event-driven integration
Source: https://docs.aikeedo.com/development/plugins/guides/event-driven-integration
Sync Aikeedo users to an external service using domain event listeners, a cron backfill with cursors, and a multi-page admin area.
Integrations with CRMs, email platforms and analytics tools share one shape: react to events as they happen, and backfill everything else in the background. This guide builds `acme/crm` against a fictional "Acme CRM".
## The design
| Piece | Job |
| - | - |
| Event listeners | Push a contact when a user signs up or changes |
| A cron listener | Backfill existing users in batches, tracked by a cursor |
| An admin area | Store the API key, map fields, and start or stop a sync |
| An HTTP client | Talk to the external API through the PSR-18 client |
## Listen to user events
```php src/Plugin.php theme={null}
use Acme\Crm\Listeners\PushContact;
use Acme\Crm\Listeners\SyncContacts;
use Cron\Domain\Events\CronEvent;
use Easy\EventDispatcher\Mapper\ArrayMapper;
use User\Domain\Events\UserCreatedEvent;
use User\Domain\Events\UserUpdatedEvent;
public function boot(Context $context): void
{
$this->loader->addPath(__DIR__ . '/../templates', 'acme-crm');
$this->mapper->addPath(__DIR__);
$this->events->addEventListener(UserCreatedEvent::class, PushContact::class);
$this->events->addEventListener(UserUpdatedEvent::class, PushContact::class);
$this->events->addEventListener(CronEvent::class, SyncContacts::class);
}
```
Listeners match by `instanceof`, and `EmailVerifiedEvent` and `UserEmailUpdatedEvent` both extend `UserUpdatedEvent`. Subscribing to the parent already covers them. Subscribing to both the parent and a child makes your listener run twice for the same change, which usually means two API calls and, on some platforms, a duplicated contact.
## The listener
```php src/Listeners/PushContact.php theme={null}
autoSync) {
return;
}
$user = $event->user;
if ($this->onlyVerified && !$user->isEmailVerified()) {
return;
}
try {
$this->client->upsertContact($this->mapper->toContact($user));
} catch (Throwable $th) {
// A CRM outage must never break signup.
$this->logger->error('Acme CRM push failed', [
'exception' => $th,
'user' => (string) $user->getId()->getValue(),
]);
}
}
}
```
### Map the fields
Keep mapping in one class, so the listener and the backfill can't drift apart:
```php src/ContactMapper.php theme={null}
public function toContact(UserEntity $user): array
{
return [
'external_id' => (string) $user->getId()->getValue(),
'email' => (string) $user->getEmail()->value,
'first_name' => (string) $user->getFirstName()->value,
'last_name' => (string) $user->getLastName()->value,
'subscribed' => (bool) $user->getPreferences()->marketing,
];
}
```
Respect marketing consent. `getPreferences()->marketing` says whether the user agreed to marketing contact, and pushing an unsubscribed contact into a marketing list is a compliance problem, not just a bug.
## The backfill
Existing users don't fire events, so give administrators a sync they can start. Keep its state in an option: a status, a cursor and counters.
```php src/Listeners/SyncContacts.php theme={null}
state ?? [];
if (($state['status'] ?? null) !== 'processing') {
return;
}
$cmd = new ListUsersCommand();
$cmd->setLimit(self::BATCH_SIZE);
$cmd->setOrderBy('created_at', 'asc');
if (!empty($state['cursor_id'])) {
$cmd->setCursor($state['cursor_id'], 'starting_after');
}
$synced = (int) ($state['synced'] ?? 0);
$failed = (int) ($state['failed'] ?? 0);
$cursor = $state['cursor_id'] ?? null;
$seen = 0;
foreach ($this->dispatcher->dispatch($cmd) as $user) {
$seen++;
$cursor = (string) $user->getId()->getValue();
try {
$this->client->upsertContact($this->mapper->toContact($user));
$synced++;
} catch (Throwable) {
$failed++;
}
}
$this->save([
'status' => $seen === 0 ? 'completed' : 'processing',
'cursor_id' => $cursor,
'synced' => $synced,
'failed' => $failed,
]);
}
private function save(array $sync): void
{
$this->dispatcher->dispatch(
new SaveOptionCommand('crm', json_encode(['sync' => $sync]))
);
}
}
```
Because option writes merge, saving `sync` leaves the API key and the field mapping untouched.
## The admin area
Split the pages so the flow guides an administrator through setup:
| Route | Page |
| - | - |
| `/admin/plugins/acme/crm` | Overview: connection status, sync progress, start and stop buttons |
| `/admin/plugins/acme/crm/keys` | API key |
| `/admin/plugins/acme/crm/config` | Field mapping, and whether to sync unverified users |
Redirect to the keys page until a key exists:
```php theme={null}
public function handle(ServerRequestInterface $request): ResponseInterface
{
if (!$this->apiKey) {
return new RedirectResponse('/admin/plugins/acme/crm/keys');
}
return new ViewResponse('@acme-crm/overview.twig', [
'sync' => $this->sync ?? [],
]);
}
```
Set `{% set active_menu = '/admin/plugins' %}` in these templates, and point `extra.default_url` at the overview.
### Sync controls
```php theme={null}
#[Route(path: '/plugins/acme/crm/sync/[start|stop|pause|resume:action]', method: RequestMethod::POST)]
class SyncRequestHandler extends AdminApi implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$action = $request->getAttribute('action');
match ($action) {
'start' => $this->dispatcher->dispatch(new SaveOptionCommand('crm', json_encode([
'sync' => ['status' => 'processing', 'cursor_id' => null, 'synced' => 0, 'failed' => 0],
]))),
'pause' => $this->setStatus('paused'),
'resume' => $this->setStatus('processing'),
'stop' => $this->dispatcher->dispatch(new DeleteOptionCommand('crm.sync')),
};
return new EmptyResponse(StatusCode::NO_CONTENT);
}
}
```
Nothing runs until the next cron tick. Say so on the page, and show the counters so administrators can see progress instead of wondering whether it worked.
## The HTTP client
```php src/Client.php theme={null}
public function upsertContact(array $contact): void
{
$request = $this->requestFactory
->createRequest('POST', self::BASE_URL . '/contacts')
->withHeader('Authorization', 'Bearer ' . $this->apiKey)
->withHeader('Content-Type', 'application/json')
->withBody($this->streamFactory->createStream(safe_json_encode($contact)));
$response = $this->client->sendRequest($request);
if ($response->getStatusCode() >= 300) {
throw new RuntimeException('Acme CRM error: ' . $response->getStatusCode());
}
}
```
Cache read-only lookups, such as the list of available audiences, with the PSR-6 pool so your settings page doesn't call the API on every render.
## Rate limits and daily jobs
For work that should run at most once a day, store a timestamp and return early:
```php theme={null}
$lastRun = (int) ($this->state['last_run'] ?? 0);
if ($lastRun + 86400 > time()) {
return;
}
```
For providers with strict rate limits, lower the batch size and use a bulk endpoint where one exists.
## Testing
Creating a user pushes exactly one contact, not two.
Updating a user's name updates the contact.
With auto-sync off, nothing is pushed.
A backfill processes in batches across ticks, and resumes after a pause.
Stopping a sync clears its state, and starting again begins from the top.
With the API unreachable, signup still works and the failure is logged.
## Official integrations
For email platforms, these official integrations are available on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Related
* [Events and cron](/development/plugins/events-and-cron)
* [Admin settings pages](/development/plugins/admin-settings-pages)
* [Core event catalog](/development/core/events)
# Add an import adapter
Source: https://docs.aikeedo.com/development/plugins/guides/import-adapter
Let users migrate conversations from another service into Aikeedo by implementing a migration adapter that a background job drives.
Aikeedo imports conversation archives in the background: a user uploads a file, a job is created, and the cron tick feeds the file through an adapter in batches. A plugin supplies the adapter for a new source.
## How an import runs
```mermaid theme={null}
flowchart LR
A[User uploads an archive] --> B[ImportJobEntity created]
B --> C[Cron tick]
C --> D[Adapter found by source key]
D --> E["prepare(job)"]
E --> F["parse(job, offset, limit)"]
F --> G[Conversations persisted, duplicates skipped]
G --> H{More left?}
H -- yes --> C
H -- no --> I["cleanup(job)"]
```
The job tracks its own progress, so each tick continues where the last one stopped. Batches are bounded, which keeps a large archive from exhausting memory or time.
## The interface
```php theme={null}
namespace Import\Domain\Services;
interface MigrationAdapterInterface
{
public function getName(): string;
public function getDescription(): string;
public function supports(FileEntity $file): bool;
public function prepare(ImportJobEntity $job): void;
public function parse(ImportJobEntity $job, int $offset = 0, ?int $limit = null): Generator;
public function cleanup(ImportJobEntity $job): void;
public function count(FileEntity $file): int;
}
```
| Method | Contract |
| - | - |
| `getName()`, `getDescription()` | Shown in the import UI |
| `supports(FileEntity)` | Whether this adapter can read the uploaded file |
| `count(FileEntity)` | How many conversations the archive holds, used for progress |
| `prepare(ImportJobEntity)` | One-time setup per run, such as extracting the archive |
| `parse(ImportJobEntity, int $offset, ?int $limit)` | Yields `ConversationEntity` objects, starting at `$offset` |
| `cleanup(ImportJobEntity)` | Removes temporary files; runs after every batch |
## Yield conversations
Each yielded conversation must carry the source's own identifier in its metadata as `_original_id`. The runner uses it to build a stable external ID, which is how re-running an import skips what's already there.
```php src/NotesAdapter.php theme={null}
getObjectKey()->value, '.zip');
}
#[Override]
public function count(FileEntity $file): int
{
return count($this->read($file));
}
#[Override]
public function prepare(ImportJobEntity $job): void
{
// Extract once per run, into a working directory under var/.
}
#[Override]
public function parse(
ImportJobEntity $job,
int $offset = 0,
?int $limit = null
): Generator {
$threads = array_slice(
$this->read($job->getFile()),
$offset,
$limit
);
foreach ($threads as $thread) {
$conversation = new ConversationEntity(
$job->getWorkspace(),
$job->getUser(),
new Title($thread->title),
);
// Required: the runner needs it for duplicate detection.
$conversation->setMeta(new Meta(['_original_id' => $thread->id]));
foreach ($thread->messages as $message) {
$conversation->addMessage(
$message->role === 'user'
? MessageEntity::userMessage($conversation, new Content($message->text))
: MessageEntity::assistantMessage($conversation, new Content($message->text))
);
}
yield $conversation;
}
}
#[Override]
public function cleanup(ImportJobEntity $job): void
{
// Delete anything prepare() created.
}
}
```
Check the exact constructor and factory signatures of `ConversationEntity` and `MessageEntity` in the source before you build them: they take value objects, and the message factories set the role for you.
### Rules
* **Respect `$offset` and `$limit`.** Yielding the whole archive on the first call defeats the batching, and large imports will time out.
* **Yield, don't persist.** The runner persists, deduplicates and counts.
* **Be restartable.** `prepare()` and `parse()` run again on the next tick, so make both safe to repeat.
* **Set `_original_id` on every conversation.** Without it, the conversation is skipped.
* **Keep memory flat.** Stream the archive rather than decoding it all into an array where you can.
## Register the adapter
The runner finds an adapter by matching the job's source against the **key** you registered under, so the key must equal the source value your upload endpoint sets:
```php src/Plugin.php theme={null}
use Import\Domain\Services\MigrationAdapterInterface;
use Shared\Infrastructure\Collections\ServiceCollectionInterface;
public function boot(Context $context): void
{
$this->services->add(
NotesAdapter::LOOKUP_KEY,
NotesAdapter::class,
MigrationAdapterInterface::class
);
}
```
## Create jobs
Give users a page to upload their archive, then create the file and the job:
```php theme={null}
use Import\Domain\Entities\ImportJobEntity;
use Import\Domain\ValueObjects\ImportSource;
$file = $this->store($request->getUploadedFiles()['file']);
$job = new ImportJobEntity(
$workspace,
$user,
new ImportSource(NotesAdapter::LOOKUP_KEY),
$file,
);
$this->jobs->add($job);
```
Store the upload with `FileSystemInterface` under `var/`, not the CDN: it's a working file, not something to serve. Poll the job from your page to show progress, and remember that nothing happens until a cron tick runs.
## Testing
A small archive imports completely, and conversations appear in the library.
A large archive imports across several cron ticks without timing out.
Re-importing the same archive skips existing conversations instead of duplicating them.
A corrupt archive fails the job with a readable error, rather than looping forever.
Temporary files are removed when the job finishes.
## Official import adapters
The official Migration plugin already imports from ChatGPT, Claude and Grok. It's available on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Related
* [Events and cron](/development/plugins/events-and-cron)
* [Files and storage](/development/plugins/files-and-storage)
* [Background jobs internals](/development/core/background-jobs-and-cron)
# Build a payment gateway
Source: https://docs.aikeedo.com/development/plugins/guides/payments/building-a-gateway
Implement, register and configure an Aikeedo payment gateway plugin, from the manifest to a working hosted checkout.
This guide builds `acme/demo-pay`, a gateway for a fictional provider with a hosted checkout page and HMAC-signed webhooks. It's a complete implementation you can adapt to a real provider.
## Prerequisites
* A working plugin skeleton. Follow the [plugin quickstart](/development/plugins/quickstart) first.
* An understanding of the [payment lifecycle](/development/plugins/guides/payments/overview).
## Step 1: Register the gateway
```php src/Plugin.php theme={null}
loader->addPath(__DIR__ . '/../templates', 'demo-pay');
$this->factory->register(DemoPay::LOOKUP_KEY, DemoPay::class);
$this->mapper->addPath(__DIR__);
}
}
```
Set `extra.default_url` in the manifest to `/admin/settings/payments/demo-pay`, which is where the payments list links.
## Step 2: Implement the gateway
```php src/DemoPay.php theme={null}
isEnabled && $this->secretKey !== null;
}
#[Override]
public function getName(): string
{
return 'DemoPay';
}
#[Override]
public function getLogo(): string
{
return file_get_contents(__DIR__ . '/../assets/logo.svg');
}
#[Override]
public function getButtonBackgroundColor(): string
{
return '#0f172a';
}
#[Override]
public function getButtonTextColor(): string
{
return '#ffffff';
}
#[Override]
public function purchase(OrderEntity $order): UriInterface|PurchaseToken|string
{
// Convert into the currency the account is configured for.
[$price, $currency] = $this->helper->convert(
$order->getTotalPrice(),
$order->getCurrencyCode(),
$this->currency,
);
$fraction = Currencies::getFractionDigits($currency->value);
try {
$session = $this->client->post('/checkout-sessions', [
// Minor units, as the provider expects
'amount' => $price->value,
'currency' => $currency->value,
'reference' => (string) $order->getId()->getValue(),
'success_url' => $this->helper->generateReturnUrl($order, self::LOOKUP_KEY),
'cancel_url' => $this->helper->generateCancelUrl($order, self::LOOKUP_KEY),
'webhook_url' => $this->helper->generateWebhookUrl($order, self::LOOKUP_KEY),
]);
} catch (Throwable $th) {
throw new PaymentException($th->getMessage(), previous: $th);
}
// Remember the provider's id before leaving the site.
$order->initiatePayment(
new PaymentGateway(self::LOOKUP_KEY),
new ExternalId($session->id),
);
return new Uri($session->url);
}
#[Override]
public function completePurchase(OrderEntity $order, array $params = []): string
{
$sessionId = (string) $order->getExternalId()->value;
if (!$sessionId) {
throw new PaymentException('Missing payment reference');
}
$session = $this->client->get('/checkout-sessions/' . $sessionId);
// Verify: right order, and actually paid.
if ($session->reference !== (string) $order->getId()->getValue()) {
throw new PaymentException('Payment reference mismatch');
}
if ($session->status !== 'paid') {
throw new PaymentException('Payment is not completed');
}
return (string) $session->payment_id;
}
#[Override]
public function cancelSubscription(string $id): void
{
try {
$this->client->post('/subscriptions/' . $id . '/cancel', []);
} catch (Throwable $th) {
// Already cancelled at the provider: nothing to do.
throw new PaymentException($th->getMessage(), previous: $th);
}
}
#[Override]
public function getWebhookHandler(): string|WebhookHandlerInterface
{
return WebhookHandler::class;
}
}
```
### Key points
* **`isEnabled()` must check the configuration too.** A gateway that's switched on but missing credentials shouldn't appear at checkout.
* **Store the provider's reference with `initiatePayment()`** before redirecting, so `completePurchase()` can find the payment even when the provider sends no parameters back.
* **Verify on the server.** Re-fetch the payment and compare the amount and your order reference. Never trust parameters in the return URL.
* **Wrap provider failures in `PaymentException`**, so the user gets a readable message instead of a `500`.
## Step 3: Amounts and currency
Aikeedo stores money as an integer in minor units, along with a currency code. Two conversions matter:
```php theme={null}
// 1. Exchange rate, only when the gateway bills in a fixed currency
[$price, $currency] = $this->helper->convert(
$order->getTotalPrice(),
$order->getCurrencyCode(),
$this->currency,
);
// 2. Minor to major units, when the provider expects a decimal amount
$fraction = Currencies::getFractionDigits($currency->value);
$major = number_format($price->value / (10 ** $fraction), $fraction, '.', '');
```
`Helper::convert()` returns the original amount and currency unchanged if the exchange fails, so check the returned currency before you send it to the provider.
Totals already include tax and any coupon:
| Method | Returns |
| - | - |
| `getTotalPrice()` | What the customer pays, tax included, honoring a coupon's cycle limit |
| `getTotalPrice(true)` | The same, but ignoring a coupon's cycle count, for providers that can't express "discount for the first N cycles" |
| `getSubtotal()` | The plan price before tax and discounts |
| `getDiscount()` | The coupon amount |
| `getTaxAmount()`, `getTaxLines()` | Tax total, and the individual lines when the provider wants an itemized breakdown |
## Step 4: The settings page
```php src/SettingsRequestHandler.php theme={null}
#[Route(path: '/settings/payments/demo-pay', method: RequestMethod::GET)]
class SettingsRequestHandler extends AbstractAdminViewRequestHandler implements
RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new ViewResponse('@demo-pay/settings.twig', [
'currencies' => Currencies::getNames(),
]);
}
}
```
The template follows the standard settings form, described in [Admin settings pages](/development/plugins/admin-settings-pages). Include:
* An enable toggle, `demo_pay[is_enabled]`
* API credentials
* A currency select, or an "inherit from billing settings" option
* The webhook URL, with ``, so the administrator can paste it into the provider's dashboard:
```twig theme={null}
{{ option.site.url }}/webhooks/demo-pay
```
For separate test and live credentials, use the mirroring pattern from the settings page guide, so your PHP always injects one flat key.
## Step 5: Test it
Enter sandbox credentials, enable it, and confirm it appears on the checkout page for a paid plan.
Buy a one-time plan in the provider's sandbox. You should land on the receipt page with the order marked paid, and the workspace should receive its credits.
Start a checkout and cancel at the provider. You should return to the billing page, with the order still unpaid and no credits granted.
Use an invalid API key. Checkout should show your error message, not a `500`.
Orders are only fulfilled after the provider confirms the payment.
## Official gateways
Before you build a gateway, check whether the provider is already covered by an official one on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Related
* [Checkout flows](/development/plugins/guides/payments/checkout-flows)
* [Subscriptions and renewals](/development/plugins/guides/payments/subscriptions-and-renewals)
* [Webhooks](/development/plugins/guides/payments/webhooks)
* [Payment gateway settings](/integrations/payment-gateways/overview)
# Checkout flows
Source: https://docs.aikeedo.com/development/plugins/guides/payments/checkout-flows
Choose how an Aikeedo payment gateway takes the user through payment: a hosted redirect, an embedded form on your own page, or an offline token.
`purchase()` decides the shape of your checkout. There are three workable flows, and the right one depends on what your provider offers.
## Flow 1: Hosted redirect
The provider hosts the payment page. This is the simplest flow and the safest, since card data never touches the installation.
```php theme={null}
public function purchase(OrderEntity $order): UriInterface|PurchaseToken|string
{
$session = $this->client->post('/checkout-sessions', [
'amount' => $order->getTotalPrice()->value,
'reference' => (string) $order->getId()->getValue(),
'success_url' => $this->helper->generateReturnUrl($order, self::LOOKUP_KEY),
'cancel_url' => $this->helper->generateCancelUrl($order, self::LOOKUP_KEY),
]);
$order->initiatePayment(
new PaymentGateway(self::LOOKUP_KEY),
new ExternalId($session->id),
);
return new Uri($session->url);
}
```
Returning a `UriInterface` makes the browser follow it. When the user comes back, `completePurchase()` verifies the payment.
## Flow 2: Your own checkout page
Use this when the provider needs a client-side SDK, or when you must collect something extra, such as a bank choice.
Return a **relative** URI, which resolves against the site root:
```php theme={null}
return new Uri('app/billing/orders/' . $order->getId()->getValue() . '/demo-pay');
```
Then serve that page from your plugin, extending the billing view base class:
```php src/CheckoutView.php theme={null}
dispatcher->dispatch(
new ReadOrderCommand($request->getAttribute('id'))
);
if ($order->isPaid()) {
return new RedirectResponse(
'/app/billing/orders/' . $order->getId()->getValue() . '/receipt'
);
}
return new ViewResponse('@demo-pay/checkout.twig', [
'order' => new OrderResource($order),
'public_key' => $this->publicKey,
]);
}
}
```
`BillingView` puts the route under `/app/billing` and requires an authenticated user, so the page sits inside the app with the normal chrome.
The page loads the provider's script and, on success, sends the user to the callback URL:
```twig templates/checkout.twig theme={null}
{% extends "/layouts/main.twig" %}
{% block template %}
{% endblock %}
{% block scripts %}
{{ parent() }}
{% endblock %}
```
If you need a server-side step before redirecting, such as creating a mandate from a form, add a POST endpoint under the same base class and redirect from there.
## Flow 3: Offline or token-based
Methods that settle outside Aikeedo, such as a bank transfer, return a `PurchaseToken`. The user goes to the receipt page, which shows your instructions, and an administrator marks the order paid later.
```php theme={null}
$token = Uuid::uuid4()->toString();
$order->initiatePayment(
new PaymentGateway(self::LOOKUP_KEY),
new ExternalId($token),
);
return new PurchaseToken($token);
```
Implement `OfflinePaymentGatewayInterface` so the method is listed with the manual options rather than as a branded button.
The generic checkout buttons don't do anything special with the token beyond sending the user to the receipt. Only the built-in card form consumes a token client-side.
## Flow 4: Charge immediately
If the payment is already complete when `purchase()` runs, for example because you charged a saved payment method, return the provider reference as a plain `string`. Aikeedo pays and fulfils the order right away, without a callback.
```php theme={null}
$charge = $this->client->post('/charges', [...]);
return (string) $charge->id;
```
Only do this when the charge is genuinely settled. Returning a reference for a pending payment grants credits that may never be paid for.
## Where the reference lives
`completePurchase()` must return the provider reference, because it becomes the order's external ID and, for recurring plans, the subscription's. There are two ways to get it:
| Approach | How | When to use |
| - | - | - |
| **Store it before redirecting** | `$order->initiatePayment(new PaymentGateway(KEY), new ExternalId($id))` in `purchase()`, then read `$order->getExternalId()` in `completePurchase()` | The provider returns no useful parameters, or you don't trust what it sends |
| **Read it from the callback** | `$params['payment_id'] ?? null` in `completePurchase()` | The provider appends its reference to the return URL |
Either way, verify against the provider before returning.
```php theme={null}
public function completePurchase(OrderEntity $order, array $params = []): string
{
$id = $params['payment_id'] ?? (string) $order->getExternalId()->value;
if (!$id) {
throw new PaymentException('Missing payment reference');
}
$payment = $this->client->get('/payments/' . $id);
if ($payment->reference !== (string) $order->getId()->getValue()) {
throw new PaymentException('Payment reference mismatch');
}
if ($payment->status !== 'succeeded') {
throw new PaymentException('Payment is not completed');
}
return (string) $payment->id;
}
```
Compare the amount too when the provider lets the payer change it. An unchecked callback is an invitation to pay 1 unit for a 99 unit plan.
## Callback URLs
| Helper | Returns |
| - | - |
| `generateReturnUrl($order, $key)` | `/payment-callback/{orderId}/{key}` on the configured site domain |
| `generateCancelUrl($order, $key)` | The billing page, for abandoned payments |
| `generateWebhookUrl($order, $key)` | `/webhooks/{key}` |
The callback accepts both `GET` and `POST`, and `completePurchase()` receives the query parameters merged with the parsed body, so a provider that posts its result works the same as one that redirects.
## Related
* [Build a gateway](/development/plugins/guides/payments/building-a-gateway)
* [Subscriptions and renewals](/development/plugins/guides/payments/subscriptions-and-renewals)
* [Webhooks](/development/plugins/guides/payments/webhooks)
# Payment gateways
Source: https://docs.aikeedo.com/development/plugins/guides/payments/overview
How Aikeedo processes a payment end to end, and which interfaces a gateway plugin implements to take part.
A payment gateway plugin connects Aikeedo's checkout to a payment provider. Aikeedo owns the order, the subscription and the credits; your plugin owns the conversation with the provider.
This page applies to Aikeedo 5.x. Class names, paths and signatures match the 5.0 source code.
## The payment lifecycle
```mermaid theme={null}
sequenceDiagram
participant U as User
participant A as Aikeedo
participant G as Your gateway
participant P as Provider
U->>A: Opens checkout, picks a gateway
A->>A: CreateOrderCommand
A->>G: purchase(order)
G->>P: Create payment or subscription
P-->>G: Redirect URL or reference
G-->>A: UriInterface, PurchaseToken or string
A-->>U: Redirect to the provider
U->>P: Pays
P-->>U: Returns to /payment-callback/{order}/{key}
A->>G: completePurchase(order, params)
G->>P: Verify the payment
G-->>A: External reference
A->>A: PayOrderCommand, FulfillOrderCommand
A-->>U: Receipt page
P->>A: POST /webhooks/{key}
A->>G: Webhook handler
```
### Step by step
The checkout page asks the gateway factory for every registered gateway, skips the ones that aren't enabled, and groups the rest: card, crypto, offsite and offline. A gateway that implements `PlanAwarePaymentGatewayInterface` is hidden for plans it says it can't handle.
The browser posts to `/api/billing/checkout` with the plan, the gateway key and any coupon. Aikeedo creates an order, then calls your `purchase()`.
What you return decides what happens next:
| Return type | Aikeedo's behavior |
| - | - |
| `Psr\Http\Message\UriInterface` | Redirects the browser there. A relative URI resolves against the site root, which is how a gateway can render its own checkout page. |
| `Billing\Infrastructure\Payments\PurchaseToken` | Sends the user to the order's receipt page with the token available. Offline methods, such as a bank transfer, use this. |
| `string` | Treated as the provider reference for an already-completed charge: Aikeedo pays and fulfils the order immediately. |
The provider sends the user back to `/payment-callback/{orderId}/{gatewayKey}`. Aikeedo calls `completePurchase($order, $params)` with the query string, merged with the parsed body for a POST. Verify the payment and return the provider's reference.
The order is paid with the reference you returned, then fulfilled. For a recurring plan a subscription is created, carrying that same reference. Any previous subscription is cancelled. The user lands on the receipt.
Events such as cancellations arrive at `POST /webhooks/{gatewayKey}` and are routed to your webhook handler.
## Interfaces
Every gateway implements `Billing\Infrastructure\Payments\PaymentGatewayInterface`:
| Method | Purpose |
| - | - |
| `isEnabled(): bool` | Whether the administrator turned it on and configured it |
| `getName(): string` | Display name |
| `purchase(OrderEntity $order): UriInterface\|PurchaseToken\|string` | Starts the payment |
| `completePurchase(OrderEntity $order, array $params = []): string` | Verifies it, and returns the provider reference |
| `cancelSubscription(string $id): void` | Cancels a subscription at the provider |
| `getWebhookHandler(): string\|WebhookHandlerInterface` | The handler class, or an instance |
Add the marker interfaces that describe your gateway:
| Interface | Effect |
| - | - |
| `OffsitePaymentGatewayInterface` | Rendered as a branded button. Adds `getLogo()`, `getButtonBackgroundColor()` and `getButtonTextColor()`. Most gateways use this. |
| `OfflinePaymentGatewayInterface` | Listed with the manual methods. Adds `getIcon()`. |
| `CardPaymentGatewayInterface` | Marks the gateway as the card form provider |
| `CryptoPaymentGatewayInterface` | Grouped with crypto methods |
| `PlanAwarePaymentGatewayInterface` | Adds `supportsPlan(PlanEntity $plan): bool` so you can hide the gateway for plans you can't bill |
Adding a `supportsPlan()` method without implementing `PlanAwarePaymentGatewayInterface` does nothing: the checkout page only calls it on gateways that declare the interface, so your gateway appears for every plan.
## Where things live
| Path | Contents |
| - | - |
| `src/Billing/Infrastructure/Payments/` | The interfaces, the factory, `PurchaseToken`, `Helper` and the exceptions |
| `POST /api/billing/checkout` | Creates the order and calls `purchase()` |
| `/payment-callback/{orderId}/{gateway}` | Calls `completePurchase()` |
| `POST /webhooks/{gateway}` | Routes to your webhook handler |
| `/admin/settings/payments` | Lists registered gateways, linking to `/admin/settings/payments/{key}` |
## Exceptions
| Exception | When to throw it |
| - | - |
| `Billing\Infrastructure\Payments\Exceptions\PaymentException` | The payment can't start or can't be verified. Checkout returns `422` with your message; on the callback the user goes to the receipt with the order unpaid. |
| `Billing\Infrastructure\Payments\Exceptions\WebhookException` | A webhook is invalid, for example a bad signature. Returns `400`. |
## Official gateways
Aikeedo ships with Stripe, PayPal, bank transfer and manual payment gateways. Official gateways for more regions are available on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Continue
A complete, working gateway plugin.
Hosted redirects, embedded forms and tokens.
Recurring billing, trials and cancellation.
Verify, route and act on provider events.
# Subscriptions and renewals
Source: https://docs.aikeedo.com/development/plugins/guides/payments/subscriptions-and-renewals
Handle recurring billing in an Aikeedo payment gateway: provider-managed schedules, merchant-initiated charges, trials and cancellation.
For a recurring plan, Aikeedo creates a subscription when the order is fulfilled and tracks usage against it. Who charges the customer each period, though, depends on your provider.
## What Aikeedo does
When an order for a monthly, yearly or lifetime plan is fulfilled:
1. A subscription is created from the order, copying the order's **external ID** and **payment gateway**.
2. The workspace is subscribed to the plan, which grants its credits.
3. Usage resets are scheduled: the first is 30 days out, or after the trial period.
The value your `completePurchase()` returned becomes that external ID. Everything later, including cancellation and webhook lookups, finds the subscription by it, so return the identifier you'll need again:
| Provider model | Return from `completePurchase()` |
| - | - |
| The provider manages the schedule | The provider's subscription ID |
| You charge each period yourself | The saved payment method or mandate ID |
| One-time payment, no recurrence | The charge or transaction ID |
## Usage resets are not charges
Aikeedo never charges a card by itself. The renewal cron resets usage and dispatches `Billing\Domain\Events\SubscriptionUsageResetEvent`. Money only moves if your provider charges on its own schedule, or your plugin listens to that event and charges.
Usage resets run every 30 days, including for yearly plans, because the credit allowance is monthly. Use `SubscriptionEntity::getNextBillingAt()` when you need the actual next payment date, which follows the plan's billing cycle.
## Model A: the provider manages the schedule
Most providers do. Create a subscription during `purchase()`, and let the provider bill on its own:
```php theme={null}
public function purchase(OrderEntity $order): UriInterface|PurchaseToken|string
{
$snapshot = $order->getPlan();
if (!$snapshot->getBillingCycle()->isRecurring()) {
return $this->createOneTimeCheckout($order);
}
$plan = $this->findOrCreateProviderPlan($order);
$session = $this->client->post('/subscriptions', [
'plan' => $plan->id,
'reference' => (string) $order->getId()->getValue(),
'trial_days' => $order->getTrialPeriodDays()->value,
'success_url' => $this->helper->generateReturnUrl($order, self::LOOKUP_KEY),
]);
$order->initiatePayment(
new PaymentGateway(self::LOOKUP_KEY),
new ExternalId($session->id),
);
return new Uri($session->url);
}
```
Then `completePurchase()` returns the provider's subscription ID, and your webhook handler reacts to cancellations and failed payments.
### Creating provider plans
Providers usually need their own plan object, with a price and an interval. Derive it from the order rather than the Aikeedo plan alone, because tax and coupons change what the customer actually pays. A reliable approach is to look up a plan keyed by a fingerprint, and create it if it's missing:
```php theme={null}
$fingerprint = implode('/', array_filter([
(string) $order->getPlan()->getId()->getValue(),
$order->getCoupon()?->getId()->getValue(),
$order->getTaxAmount()->value > 0 ? 'tax:' . $order->getTaxAmount()->value : null,
]));
```
Store that fingerprint in the provider plan's name or metadata, and reuse the plan for every customer with the same combination.
Use `getTotalPrice(true)` when the provider can't express "discount for the first N cycles". It applies the coupon to the recurring price instead, which is the closest equivalent most providers support.
## Model B: you charge each period
If the provider only gives you a saved payment method or a mandate, charge when Aikeedo resets usage:
```php src/Plugin.php theme={null}
use Billing\Domain\Events\SubscriptionUsageResetEvent;
use Easy\EventDispatcher\Mapper\ArrayMapper;
public function boot(Context $context): void
{
$this->mapper->addEventListener(
SubscriptionUsageResetEvent::class,
ChargeRecurringPayment::class
);
}
```
```php src/Listeners/ChargeRecurringPayment.php theme={null}
subscription;
// Only subscriptions that belong to this gateway.
if ($subscription->getPaymentGateway()->value !== DemoPay::LOOKUP_KEY) {
return;
}
try {
$this->client->post('/charges', [
'payment_method' => (string) $subscription->getExternalId()->value,
'amount' => $subscription->getPlan()->getPrice()->value,
]);
} catch (Throwable $th) {
$this->logger->error('DemoPay renewal failed', ['exception' => $th]);
// No payment, no service: end the subscription.
$this->dispatcher->dispatch(
new EndSubscriptionCommand($subscription->getId())
);
}
}
}
```
Always filter by gateway key first: the event fires for every subscription in the installation, not just yours.
## Trials
`OrderEntity::getTrialPeriodDays()` carries the plan's trial length. Providers support trials differently, and all of these appear in practice:
| Approach | Notes |
| - | - |
| Native trial | Pass the trial length, and let the provider start billing afterwards |
| Delayed start | Create the subscription with a start date after the trial |
| Authorize and void | Verify the card with a small authorization, then release it |
| Charge and refund | Only when nothing else is available, because refunds cost fees |
When the installation has "trial without payment" enabled and the plan has trial days, Aikeedo cancels the subscription right after creating it, so the customer gets the trial period and nothing renews. Don't fight that: if the order's trial days are set and no payment was taken, return a reference that reflects reality.
## Cancellation
```php theme={null}
public function cancelSubscription(string $id): void
{
$this->client->post('/subscriptions/' . $id . '/cancel', []);
}
```
Aikeedo calls this from the user's billing page, from the admin panel, when a workspace switches plans, and while handling your own webhook.
Make `cancelSubscription()` idempotent. It's called even when the provider was the one that cancelled, so a second cancellation must not throw. Treat "already cancelled" and "not found" as success.
Cancelling in Aikeedo sets the subscription to end at its next renewal date, so the customer keeps what they paid for. A separate cron job ends expired subscriptions and moves the workspace to the fallback plan.
| Command | Effect |
| - | - |
| `Billing\Application\Commands\CancelSubscriptionCommand` | Ends at period end, and calls your `cancelSubscription()` |
| `Billing\Application\Commands\EndSubscriptionCommand` | Ends immediately, moves the workspace to the fallback plan, and does **not** call the provider |
Use `CancelSubscriptionCommand` for a normal cancellation, and `EndSubscriptionCommand` when service must stop now, such as after a failed renewal or a chargeback.
## Testing checklist
A recurring plan creates a subscription whose external ID is the one your provider knows.
A trial starts without charging, and the first charge lands when the trial ends.
Cancelling in Aikeedo cancels at the provider, and cancelling at the provider cancels in Aikeedo.
Cancelling twice doesn't throw.
A failed renewal ends the subscription instead of leaving free access.
Switching plans cancels the previous subscription exactly once.
## Related
* [Webhooks](/development/plugins/guides/payments/webhooks)
* [Build a gateway](/development/plugins/guides/payments/building-a-gateway)
* [Billing internals](/development/core/billing-subsystem)
* [Plans, snapshots and subscriptions](/billing/plans-snapshots-subscriptions)
# Payment webhooks
Source: https://docs.aikeedo.com/development/plugins/guides/payments/webhooks
Receive and verify provider webhooks in an Aikeedo payment gateway, and act on them safely and idempotently.
Providers report what happens after checkout through webhooks: a subscription cancelled, a renewal that failed, an asynchronous payment that finally settled. Aikeedo routes them to your gateway automatically.
## The endpoint
Every gateway shares one route:
```text theme={null}
POST https://your-domain.com/webhooks/{gateway-key}
```
The request handler resolves your gateway from its key, asks for `getWebhookHandler()`, and calls `handle()`. Return the URL from `Helper::generateWebhookUrl()`, and show it on your settings page so administrators can paste it into the provider's dashboard.
```php theme={null}
#[Override]
public function getWebhookHandler(): string|WebhookHandlerInterface
{
// A class-string is resolved from the container, so dependencies are injected.
return WebhookHandler::class;
}
```
The webhook route runs with error handling only. There's no session, no authentication and no CSRF protection, because the caller is a machine. Your signature check is the authentication.
## A handler
```php src/WebhookHandler.php theme={null}
verify($request);
match ($payload->type ?? null) {
'subscription.cancelled' => $this->cancelSubscription($payload),
default => null,
};
}
private function verify(ServerRequestInterface $request): object
{
// Fail closed: without a secret nothing can be verified.
if (!$this->secret) {
throw new WebhookException('Webhook secret is not configured');
}
$body = (string) $request->getBody();
$signature = $request->getHeaderLine('X-DemoPay-Signature');
$expected = hash_hmac('sha256', $body, $this->secret);
if (!$signature || !hash_equals($expected, $signature)) {
throw new WebhookException('Invalid webhook signature');
}
$payload = json_decode($body);
if (!is_object($payload)) {
throw new WebhookException('Invalid webhook payload');
}
return $payload;
}
private function cancelSubscription(object $payload): void
{
try {
$subscription = $this->dispatcher->dispatch(
ReadSubscriptionCommand::createByExternalId(
DemoPay::LOOKUP_KEY,
(string) $payload->subscription_id
)
);
} catch (SubscriptionNotFoundException) {
// Unknown subscription: acknowledge so the provider stops retrying.
$this->logger->info('DemoPay webhook for unknown subscription');
return;
}
$this->dispatcher->dispatch(
new CancelSubscriptionCommand($subscription->getId())
);
}
}
```
## Verify every request
Compute the signature over `(string) $request->getBody()`, not over a re-encoded array. Re-encoding changes key order and escaping, and the signature won't match.
Use `hash_equals()`, never `==`.
If the secret isn't configured, reject the request. Skipping verification "until it's set up" means anyone can cancel subscriptions or mark orders paid.
Confirm the event refers to an object you know, and that the amount and currency match the order before you treat it as payment.
## Responses
| Outcome | What to do |
| - | - |
| Handled successfully | Return normally. Aikeedo responds with an empty `200`. |
| Invalid signature or payload | Throw `WebhookException`, which returns `400`. |
| Event you don't handle | Return normally. Don't throw, or the provider will retry forever. |
| Unknown subscription or order | Log it and return normally, unless you expect the object to appear later. |
## Acting on events
| Event | Command to dispatch |
| - | - |
| Subscription cancelled at the provider | `CancelSubscriptionCommand`, which ends it at period end |
| Payment failed and access must stop now | `EndSubscriptionCommand`, which ends immediately and applies the fallback plan |
| Asynchronous payment settled | `PayOrderCommand`, then `FulfillOrderCommand` |
```php theme={null}
use Billing\Application\Commands\FulfillOrderCommand;
use Billing\Application\Commands\PayOrderCommand;
use Billing\Application\Commands\ReadOrderCommand;
use Billing\Domain\Exceptions\AlreadyFulfilledException;
use Billing\Domain\Exceptions\AlreadyPaidException;
$order = $this->dispatcher->dispatch(new ReadOrderCommand($payload->reference));
try {
$this->dispatcher->dispatch(
new PayOrderCommand($order->getId(), DemoPay::LOOKUP_KEY, $payload->payment_id)
);
} catch (AlreadyPaidException) {
// A concurrent callback got there first; continue to fulfilment.
} catch (AlreadyFulfilledException) {
return;
}
$this->dispatcher->dispatch(new FulfillOrderCommand($order->getId()));
```
Fulfilling from a webhook must mirror what the callback does. If the workspace had a previous subscription, cancel it as well, or the customer ends up with two active subscriptions.
## Idempotency
Providers retry, deliver out of order, and sometimes deliver twice.
Look up state before you change it, and make repeated deliveries a no-op.
Rely on the exceptions: `AlreadyPaidException` and `AlreadyFulfilledException` tell you the work is already done.
Never grant credits directly from a webhook. Go through the order commands, which enforce the state machine.
Log the provider's event ID so duplicates are visible while debugging.
## Testing
* Use the provider's dashboard to replay events at your installation.
* For local development, expose your machine with a tunnel, and set the site URL so the generated webhook URL is reachable.
* Send a request with a wrong signature and confirm you get a `400`.
* Send an unknown event type and confirm you get a `200`.
* Watch `var/log/app-*.log` while testing.
## Related
* [Subscriptions and renewals](/development/plugins/guides/payments/subscriptions-and-renewals)
* [Build a gateway](/development/plugins/guides/payments/building-a-gateway)
* [Public endpoints](/development/plugins/public-endpoints-and-embeds)
# Add per-plan settings
Source: https://docs.aikeedo.com/development/plugins/guides/plan-config-extension
Let administrators configure your Aikeedo plugin per subscription plan, and read those settings at runtime.
Plans decide what a workspace can do. A plan config extension adds your own fields to the plan editor, stores them with the plan, and carries them into the snapshot a subscription is based on.
## The interface
```php theme={null}
namespace Billing\Infrastructure\PlanConfig;
interface PlanConfigExtensionInterface
{
public function getDefaults(): ?array;
public function getTemplatePath(): ?string;
public function getSnapshotTemplatePath(): ?string;
}
```
| Method | Purpose |
| - | - |
| `getDefaults()` | Default values, applied to plans that don't have your settings yet |
| `getTemplatePath()` | Twig template rendered in the plan editor |
| `getSnapshotTemplatePath()` | Read-only template rendered when viewing a plan snapshot |
## Implementation
```php src/PlanConfigExtension.php theme={null}
false,
'project_cap' => null,
];
}
#[Override]
public function getTemplatePath(): ?string
{
return '@acme-projects/plan.twig';
}
#[Override]
public function getSnapshotTemplatePath(): ?string
{
return '@acme-projects/plan-snapshot.twig';
}
}
```
## Register it
```php src/Plugin.php theme={null}
use Billing\Infrastructure\PlanConfig\PlanConfigExtensionInterface;
use Shared\Infrastructure\Collections\ServiceCollectionInterface;
public function boot(Context $context): void
{
$this->loader->addPath(__DIR__ . '/../templates', 'acme-projects');
$this->services->add(
PlanConfigExtension::LOOKUP_KEY,
PlanConfigExtension::class,
PlanConfigExtensionInterface::class
);
}
```
The key you register under namespaces your settings inside the plan, under `config.extensions.{key}`.
## The editor template
The plan editor is an Alpine form bound to a `model` object. Your template receives `key` and `defaults`, and binds inputs into `model.config.extensions[key]`:
```twig templates/plan.twig theme={null}
{{ p__('heading', 'Projects') }}
{{ p__('label', 'Projects enabled') }}
{{ __('Leave empty for unlimited projects.') }}
```
The editor initializes your subtree from `getDefaults()` when a plan doesn't have it yet, so you can bind directly without guarding for missing keys.
### The snapshot template
Snapshots are historical copies of a plan, so the template is read-only, and the data may predate your extension:
```twig templates/plan-snapshot.twig theme={null}
{{ p__('label', 'Projects') }}
```
Use optional chaining, since older snapshots won't contain your key.
## Read the settings at runtime
Your settings travel with the plan and its snapshots, so read them from the workspace's subscription:
```php theme={null}
use Workspace\Domain\Entities\WorkspaceEntity;
public function canCreateProject(WorkspaceEntity $workspace): bool
{
$subscription = $workspace->getSubscription();
if (!$subscription) {
return false;
}
$config = $subscription->getPlan()->getConfig();
$settings = $config->extensions[PlanConfigExtension::LOOKUP_KEY] ?? [];
if (!($settings['is_enabled'] ?? false)) {
return false;
}
$cap = $settings['project_cap'] ?? null;
return $cap === null || $this->countProjects($workspace) < $cap;
}
```
In Twig, the same data is on the workspace:
```twig theme={null}
{% set projects = workspace.subscription.plan.config.extensions.acme_projects|default({}) %}
{% if projects.is_enabled|default(false) %}
{{ p__('button', 'Projects') }}
{% endif %}
```
Aikeedo stores and displays your settings, but it doesn't enforce them. Every limit you define has to be checked by your own code, in every path that can create or consume the resource.
## Design notes
* **Use `null` for unlimited**, and say so in the field's help text.
* **Default to off.** A new extension must not silently enable a paid capability on every existing plan.
* **Keep keys stable.** They're written into every plan snapshot, and renaming one orphans historical data.
* **Keep it small.** Per-plan settings belong here; global configuration belongs on your settings page.
## Testing
Your section appears in the plan editor, both for new and existing plans.
Saving a plan persists your values, and reopening shows them.
A snapshot of the plan shows your read-only summary.
A workspace on a plan with the capability disabled is blocked by your checks.
A cap is enforced at the limit, not one over.
## Related
* [Admin settings pages](/development/plugins/admin-settings-pages)
* [Billing internals](/development/core/billing-subsystem)
* [Plans, snapshots and subscriptions](/billing/plans-snapshots-subscriptions)
# Build a storage adapter
Source: https://docs.aikeedo.com/development/plugins/guides/storage-adapter
Add a file storage backend to Aikeedo by implementing a Flysystem-based CDN adapter and registering it for administrators to select.
Aikeedo stores generated images, uploads and other user-visible files through a CDN abstraction. Administrators choose one adapter under **Settings → File storage**, and every part of the application uses it. A plugin can add more.
## The interface
```php theme={null}
namespace Shared\Infrastructure\FileSystem\Adapters;
interface CdnAdapterInterface extends AdapterInterface
{
public function isEnabled(): bool;
public function getName(): string;
public function getUrl(string $path): string;
}
```
`AdapterInterface` extends `League\Flysystem\FilesystemAdapter`, so your adapter is a Flysystem adapter with three extra methods. In practice you extend an existing Flysystem adapter and add them.
| Method | Purpose |
| - | - |
| `isEnabled()` | Whether the administrator configured and enabled this backend |
| `getName()` | Display name in the admin panel |
| `getUrl(string $path)` | Public URL for an object, or a signed URL when URL signing is on |
## Implementation
This example wraps the S3 adapter, which covers any S3-compatible provider.
```json composer.json theme={null}
{
"require": {
"heyaikeedo/composer": "^1.0.0",
"league/flysystem-aws-s3-v3": "^3.0"
}
}
```
```php src/AcmeCloud.php theme={null}
client = new S3Client([
'version' => 'latest',
'region' => $this->region ?: 'us-east-1',
'endpoint' => $this->endpoint,
'use_path_style_endpoint' => true,
'credentials' => [
'key' => (string) $this->accessKey,
'secret' => (string) $this->secretKey,
],
]);
$this->bucket = (string) $this->bucketName;
parent::__construct($this->client, $this->bucket);
}
#[Override]
public function isEnabled(): bool
{
return (bool) $this->isEnabled
&& $this->accessKey
&& $this->secretKey
&& $this->bucketName;
}
#[Override]
public function getName(): string
{
return 'Acme Cloud';
}
#[Override]
public function getUrl(string $path): string
{
if ($this->signUrls) {
$command = $this->client->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $path,
]);
return (string) $this->client
->createPresignedRequest($command, '+1 hour')
->getUri();
}
if ($this->domain) {
return rtrim($this->domain, '/') . '/' . ltrim($path, '/');
}
return rtrim((string) $this->endpoint, '/')
. '/' . $this->bucket . '/' . ltrim($path, '/');
}
}
```
The constructor runs when the adapter is first resolved, which only happens when it's the selected backend or when the admin page lists adapters. Building the client there is fine.
## Register it
```php src/Plugin.php theme={null}
use Shared\Infrastructure\FileSystem\CdnAdapterCollectionInterface;
public function __construct(
private CdnAdapterCollectionInterface $adapters,
private FilesystemLoader $loader,
private AttributeMapper $mapper,
) {}
public function boot(Context $context): void
{
$this->loader->addPath(__DIR__ . '/../templates', 'acme-storage');
$this->mapper->addPath(__DIR__);
$this->adapters->add(AcmeCloud::LOOKUP_KEY, AcmeCloud::class);
}
```
The adapter now appears under **Settings → File storage**. The administrator's choice is stored in `option.cdn.adapter`, and Aikeedo resolves your adapter for every file operation from then on.
## Settings page
The storage list links to `/admin/settings/cdn/{key}`, so declare that route and set `extra.default_url` to it:
```php theme={null}
#[Route(path: '/settings/cdn/acme-cloud', method: RequestMethod::GET)]
class SettingsRequestHandler extends AbstractAdminViewRequestHandler implements
RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new ViewResponse('@acme-storage/settings.twig');
}
}
```
Fields to include: enable toggle, endpoint, region, bucket, access key, secret key, and an optional custom domain for public URLs.
## Signed URLs
`option.cdn.sign_urls` is a global switch. When it's on, `getUrl()` must return a time-limited URL, and objects should be written privately. When it's off, objects that Aikeedo writes with public visibility must be reachable at a stable URL.
Never cache the result of `getUrl()` beyond the signature's lifetime, and never store a signed URL in the database. Aikeedo stores the object key and asks the adapter for a URL when it needs one.
## Migrating existing files
Changing the adapter doesn't move existing files: records keep the storage key they were written with. Tell administrators to copy their existing objects to the new backend before switching, or to keep the old backend reachable.
## Testing
Generate an image and confirm the file lands in your bucket.
The image displays in the library, which means `getUrl()` is right.
With URL signing on, URLs expire and are regenerated on the next page load.
Uploading a large file works, and cleanup deletes objects.
Wrong credentials produce a clear error rather than a blank page.
## Official storage adapters
The official Cloud Storage plugin already supports the most common S3-compatible providers. It's available on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Related
* [Files and storage](/development/plugins/files-and-storage)
* [Storage internals](/development/core/storage-and-files)
* [Cloud storage setup](/integrations/cloud-storage/overview)
# Build a tax engine
Source: https://docs.aikeedo.com/development/plugins/guides/tax-engine
Add a tax calculation engine to Aikeedo so orders are taxed according to your own rules or an external tax service.
A tax engine calculates the tax lines for an order at checkout. Aikeedo ships a null engine that charges nothing, and administrators select an engine under **Settings → Tax engines**.
## When it runs
Tax is calculated once, while the order is created, and the resulting lines are stored on the order. They're included in `getTotalPrice()`, so every payment gateway charges the taxed amount without doing anything extra.
If the configured engine can't be resolved, Aikeedo falls back to the null engine, so a missing plugin never blocks checkout.
## The interface
```php theme={null}
namespace Billing\Infrastructure\Tax;
interface TaxEngineInterface
{
public function getLookupKey(): string;
public function getName(): string;
public function calculate(OrderEntity $order): TaxResult;
}
```
`TaxResult` is a list of `Billing\Domain\ValueObjects\TaxLine` objects:
```php theme={null}
new TaxLine(
name: 'VAT (20%)', // shown to the customer
rate: 20.0, // percentage, or 0 for a fixed amount
amount: new TaxAmount(1999), // minor units
kind: 'percent', // 'percent' or 'fixed'
);
```
`TaxResult::getTaxTotal()` sums the lines, so return several when a jurisdiction has more than one component, such as a state and a city tax.
## Implementation
```php src/FlatTaxEngine.php theme={null}
|null Country code to rate */
#[Inject('option.flat_tax.rules')]
private ?array $rules = null,
) {}
#[Override]
public function getLookupKey(): string
{
return self::LOOKUP_KEY;
}
#[Override]
public function getName(): string
{
return 'Flat tax';
}
#[Override]
public function calculate(OrderEntity $order): TaxResult
{
$address = $order->getWorkspace()->getAddress();
$country = $address?->country;
$rate = $this->rules[$country] ?? $this->rate;
if (!$rate) {
// No rule, no tax. Returning an empty result is always safe.
return new TaxResult();
}
// Tax the amount the customer actually pays.
$base = $order->getSubtotal()->value - $order->getDiscount()->value;
$amount = (int) round($base * $rate / 100);
if ($amount <= 0) {
return new TaxResult();
}
return new TaxResult(
new TaxLine(
name: sprintf('%s (%s%%)', $this->label ?: 'Tax', $rate),
rate: (float) $rate,
amount: new TaxAmount($amount),
kind: 'percent',
)
);
}
}
```
### Rules to follow
* **Return an empty `TaxResult()` when you can't calculate**, including when an external service fails. Throwing blocks checkout entirely.
* **Work in minor units.** Round once, at the end.
* **Decide your base.** Tax the discounted amount, or the pre-discount amount, according to the rules you're implementing, and document which you chose.
* **Handle a missing address.** Many workspaces have none, and a plan may be sold without one.
* **Don't call a slow API without a timeout.** Order creation waits for you.
## Register it
```php src/Plugin.php theme={null}
use Billing\Infrastructure\Tax\TaxEngineInterface;
use Shared\Infrastructure\Collections\ServiceCollectionInterface;
public function __construct(
private ServiceCollectionInterface $services,
private FilesystemLoader $loader,
private AttributeMapper $mapper,
) {}
public function boot(Context $context): void
{
$this->loader->addPath(__DIR__ . '/../templates', 'flat-tax');
$this->mapper->addPath(__DIR__);
$this->services->add(
FlatTaxEngine::LOOKUP_KEY,
FlatTaxEngine::class,
TaxEngineInterface::class
);
}
```
The engine appears under **Settings → Tax engines**, where an administrator selects it. The selected key is stored in `option.billing.tax_engine`.
## The settings page
The tax engines list links each engine to `/admin/settings/tax-engines/{key}`, so declare that route:
```php theme={null}
#[Route(path: '/settings/tax-engines/flat-tax', method: RequestMethod::GET)]
class SettingsRequestHandler extends AbstractAdminViewRequestHandler implements
RequestHandlerInterface
{
public function __construct(
private CountryDataProvider $countries,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return new ViewResponse('@flat-tax/settings.twig', [
'countries' => $this->countries->getCountryData(),
]);
}
}
```
Set `extra.default_url` to the same path. For the form itself, see [Admin settings pages](/development/plugins/admin-settings-pages).
`Shared\Infrastructure\CountryDataProvider` gives you the country list Aikeedo already uses, so your rules editor matches the rest of the admin panel.
## Using an external tax service
For a service such as a rate API, keep the network call tight:
```php theme={null}
public function calculate(OrderEntity $order): TaxResult
{
try {
$result = $this->client->post('/calculate', [
'amount' => $order->getSubtotal()->value - $order->getDiscount()->value,
'currency' => $order->getCurrencyCode()->value,
'country' => $order->getWorkspace()->getAddress()?->country,
]);
} catch (Throwable $th) {
$this->logger->error('Tax service failed', ['exception' => $th]);
return new TaxResult();
}
return new TaxResult(...array_map(
fn(object $line) => new TaxLine(
name: $line->name,
rate: (float) $line->rate,
amount: new TaxAmount((int) $line->amount),
),
$result->lines,
));
}
```
Cache rates that rarely change with the PSR-6 pool, and never let a provider outage stop a sale.
## Testing
Checkout shows your tax lines, and the total includes them.
A workspace without an address still reaches checkout.
A zero or missing rate produces no tax line at all.
A discounted order taxes the amount you intended.
With the service unreachable, checkout still completes.
## Official tax engines
If you don't need a custom engine, these official ones are available on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Related
* [Payment gateways](/development/plugins/guides/payments/overview)
* [Tax engine settings](/integrations/tax-engines/overview)
* [Billing internals](/development/core/billing-subsystem)
# Build a vector store
Source: https://docs.aikeedo.com/development/plugins/guides/vector-store
Connect an external vector database to Aikeedo so knowledge-base embeddings are stored and searched outside the default file store.
Aikeedo turns uploaded documents and links into embeddings, then searches them when answering questions. By default those vectors are kept as JSON files in the configured storage. A vector store plugin replaces that with a real vector database.
## The interface
```php theme={null}
namespace Ai\Domain\Embedding;
interface VectorStoreInterface
{
public function isEnabled(): bool;
public function getName(): string;
public function upsert(Id $id, Embedding $embedding): void;
public function exists(Id $id): bool;
public function retrieve(Id $id): Embedding;
public function remove(Id $id): void;
public function search(
array $vector,
VectorSearchContextInterface $context,
int $limit = 5
): array;
}
```
| Method | Contract |
| - | - |
| `upsert(Id, Embedding)` | Stores every chunk of one dataset unit, replacing what was there |
| `exists(Id)` | Whether that unit has vectors stored |
| `retrieve(Id)` | Returns the stored `Embedding` |
| `remove(Id)` | Deletes the unit's vectors |
| `search(array $vector, VectorSearchContextInterface $context, int $limit)` | Returns the most similar chunks |
### The data shape
An `Ai\Domain\ValueObjects\Embedding` wraps a list of chunks, each an `EmbeddingMap` with the text and its vector:
```php theme={null}
use Ai\Domain\ValueObjects\Embedding;
use Ai\Domain\ValueObjects\EmbeddingMap;
$embedding = new Embedding(
new EmbeddingMap('The first chunk of text', [0.01, -0.22, ...]),
new EmbeddingMap('The second chunk', [0.04, 0.11, ...]),
);
// $embedding->value === [['content' => '...', 'embedding' => [...]], ...]
```
`search()` returns a ranked list in the same shape the default store produces:
```php theme={null}
[
['content' => 'The matching chunk of text', 'similarity' => 0.87],
['content' => 'Another chunk', 'similarity' => 0.81],
]
```
The `content` values are what ends up in the model's context, so return the text, not just IDs.
### The search context
```php theme={null}
interface VectorSearchContextInterface
{
public function getSearchNamespace(): string;
public function getDatasetIds(): iterable;
}
```
The context tells you **where** to search: a namespace to isolate one tenant or assistant, and the dataset unit IDs that are attached to the current conversation. Respect both, or one customer's knowledge base will leak into another's answers.
## Implementation
```php src/AcmeVectors.php theme={null}
isEnabled;
}
#[Override]
public function getName(): string
{
return 'Acme Vectors';
}
#[Override]
public function upsert(Id $id, Embedding $embedding): void
{
// Replace, don't append: a re-indexed document must not keep old chunks.
$this->remove($id);
$points = [];
foreach ($embedding->value ?? [] as $index => $chunk) {
$points[] = [
// Deterministic id, so re-indexing overwrites cleanly.
'id' => Uuid::uuid5(self::ID_NAMESPACE, $id->getValue() . ':' . $index)->toString(),
'vector' => $chunk['embedding'],
'payload' => [
'unit_id' => (string) $id->getValue(),
'content' => $chunk['content'],
],
];
}
if ($points) {
$this->client->upsert($this->collection(), $points);
}
}
#[Override]
public function exists(Id $id): bool
{
return $this->client->count($this->collection(), (string) $id->getValue()) > 0;
}
#[Override]
public function retrieve(Id $id): Embedding
{
$maps = [];
foreach ($this->client->scroll($this->collection(), (string) $id->getValue()) as $point) {
$maps[] = new EmbeddingMap($point->payload->content, $point->vector);
}
return new Embedding(...$maps);
}
#[Override]
public function remove(Id $id): void
{
$this->client->deleteByUnit($this->collection(), (string) $id->getValue());
}
#[Override]
public function search(
array $vector,
VectorSearchContextInterface $context,
int $limit = 5
): array {
$unitIds = [];
foreach ($context->getDatasetIds() as $id) {
$unitIds[] = (string) $id;
}
if (!$unitIds) {
return [];
}
$hits = $this->client->search(
$this->collection($context->getSearchNamespace()),
$vector,
$unitIds,
$limit,
);
return array_map(fn(object $hit) => [
'content' => $hit->payload->content,
'similarity' => $hit->score,
], $hits);
}
private function collection(?string $namespace = null): string
{
return $this->prefix . ($namespace ?: 'default');
}
}
```
Create collections lazily, on the first write, and make creation idempotent. Vector dimensions depend on the embedding model the installation uses, so read the dimension from the first vector you receive rather than hardcoding it.
## Register it
```php src/Plugin.php theme={null}
use Ai\Domain\Embedding\VectorStoreInterface;
use Shared\Infrastructure\Collections\ServiceCollectionInterface;
public function boot(Context $context): void
{
$this->loader->addPath(__DIR__ . '/../templates', 'acme-vectors');
$this->mapper->addPath(__DIR__);
$this->services->add(
AcmeVectors::LOOKUP_KEY,
AcmeVectors::class,
VectorStoreInterface::class
);
}
```
The store appears under **Settings → Vector databases**, and the selected key is stored in `option.embeddings.adapter`. If that key can't be resolved, Aikeedo falls back to its built-in file store.
## Settings page
Declare a route at `/admin/settings/vector-databases/acme-vectors`, and set `extra.default_url` to it. Useful fields: enable toggle, endpoint, API key, collection prefix, and a switch for per-assistant collections.
## Operational notes
* **Switching stores doesn't migrate data.** Existing knowledge bases must be re-indexed after a switch. Say so on your settings page.
* **Deletes matter.** When a document is removed, `remove()` must delete its vectors, or answers will cite deleted content.
* **Fail soft on search.** If the database is unreachable, returning an empty array degrades the answer. Throwing breaks the chat.
* **Watch the payload size.** Store the chunk text, since search results must return it.
## Testing
Upload a document to a knowledge base and confirm vectors appear in your database.
Ask a question that the document answers, and check the answer cites it.
Re-upload the same document and confirm chunks are replaced, not duplicated.
Delete the document and confirm its vectors are gone.
Two knowledge bases don't see each other's content.
## Related
* [AI models and credits](/development/plugins/ai-models-and-credits)
* [AI tools](/development/plugins/guides/ai-tool)
* [AI subsystem internals](/development/core/ai-subsystem)
# Plugin lifecycle and hooks
Source: https://docs.aikeedo.com/development/plugins/lifecycle-and-hooks
What happens when an Aikeedo plugin is installed, activated, deactivated, uninstalled or updated, and how to run your own code at each point.
A plugin moves through a small set of states. Aikeedo drives the transitions from the admin panel, and your plugin can react to each one with a lifecycle hook.
## States
| Status | Meaning |
| - | - |
| `inactive` | Installed, not booted. Every fresh installation starts here. |
| `active` | Booted on every request. |
| `failed` | The plugin crashed while booting, and Aikeedo skips it until it's reactivated. See [Debugging](/development/plugins/debugging). |
The current status lives in `extra.status` in your `composer.json`. Aikeedo writes that value when an administrator activates or deactivates the plugin, so don't manage it yourself.
## Lifecycle
```mermaid theme={null}
stateDiagram-v2
[*] --> Installed: upload archive or composer require
Installed --> Active: activate
Active --> Installed: deactivate
Active --> Failed: boot error
Failed --> Active: reactivate
Installed --> [*]: uninstall
```
### Install
An administrator uploads an archive under **Plugins → Install**, or you run `composer require` yourself. For an upload, Aikeedo:
1. Validates the archive by reading `composer.json` at its root.
2. Moves the archive into `extra/artifacts/`, which is a Composer artifact repository.
3. Backs up any existing installation of that package to `var/plugin-backup/{uuid}` and removes the old directory.
4. Runs `composer require :`, which installs the package into `extra/extensions/{vendor}/{name}` and copies `extra.public` files to the web root.
5. Forces the status to `inactive`.
6. Calls your `install()` hook.
7. Clears the application cache.
If any step fails, the backup is restored, so a failed upgrade doesn't leave a broken install behind.
### Activate and deactivate
Toggling a plugin writes the new status to `composer.json`, calls `activate()` or `deactivate()`, clears the recorded health failure when activating, and clears the cache.
Hooks run inside the current request, but `boot()` doesn't. Your plugin starts registering routes, templates and services on the **next** request.
### Update
There's no separate update step. Installing a newer archive of the same package replaces it, going through the same backup, install and hook sequence. When Aikeedo itself is updated, every installed plugin is reinstalled through Composer so its autoloader and public assets are rebuilt.
### Uninstall
Uninstalling calls `uninstall()`, runs `composer remove`, deletes `extra/extensions/{vendor}/{name}`, removes the published files recorded for the package, clears its health record and clears the cache.
Uninstalling doesn't remove your options, or any rows you wrote into core tables. Clean up in `uninstall()` if that matters for your plugin.
## Hooks
Implement any combination of these interfaces on your entry class. Each method receives the plugin's `Context`.
| Interface | Method | Runs when |
| - | - | - |
| `Plugin\Domain\Hooks\InstallHookInterface` | `install(Context $context): void` | Right after the package is installed |
| `Plugin\Domain\Hooks\ActivateHookInterface` | `activate(Context $context): void` | When an administrator activates the plugin |
| `Plugin\Domain\Hooks\DeactivateHookInterface` | `deactivate(Context $context): void` | When an administrator deactivates the plugin |
| `Plugin\Domain\Hooks\UninstallHookInterface` | `uninstall(Context $context): void` | Before the package is removed |
```php src/Plugin.php theme={null}
dispatcher->dispatch(new SaveOptionCommand(
'hello',
json_encode(['greeting' => 'Hello'])
));
}
#[Override]
public function uninstall(Context $context): void
{
// Remove everything this plugin stored.
$this->dispatcher->dispatch(new DeleteOptionCommand('hello'));
}
}
```
### Rules for hook code
* **Keep hooks idempotent.** A plugin can be installed over an existing copy, and reactivated many times.
* **Don't assume `boot()` ran.** Hooks run in a request where your plugin may not be active, so don't rely on services you register in `boot()`.
* **Fail loudly, but clean up.** An exception in `install()` aborts the installation and restores the backup, which is what you want if your setup can't complete.
* **Don't do slow work.** Hooks run inside an admin request. Schedule long tasks on cron instead. See [Events and cron](/development/plugins/events-and-cron).
## The Context object
Every hook and `boot()` receives `Plugin\Domain\Context`, the parsed manifest:
| Property | Description |
| - | - |
| `type` | `Type::PLUGIN` or `Type::THEME` |
| `name` | Package name value object |
| `version`, `description`, `homepage`, `releasedAt` | Top-level manifest metadata |
| `title`, `tagline`, `logo`, `icon`, `defaultUrl` | Values from `extra` |
| `entryClass` | The configured entry class |
| `supportChannels`, `licenses`, `authors` | Support and licensing metadata |
| `health` | The recorded boot failure, or `null` |
| `getStatus()` / `setStatus()` | Read or write `extra.status` |
```php theme={null}
public function boot(Context $context): void
{
$version = (string) $context->version->value;
}
```
`Context` carries metadata only. To reach application services, type-hint them in your constructor. See [Dependency injection](/development/plugins/dependency-injection-and-services).
## Boot order
Plugins boot after every core module bootstrapper, so all core registries exist by then. Within `boot()`:
* Register templates, routes, listeners and services.
* Read options if you need to decide what to register.
* Don't run queries, HTTP calls or migrations. `boot()` runs on every web, console and cron request.
## Related
* [Plugin overview](/development/plugins/overview)
* [Manifest reference](/development/plugins/manifest)
* [Debugging](/development/plugins/debugging)
* [Packaging and distribution](/development/plugins/packaging-and-distribution)
# Plugin localization
Source: https://docs.aikeedo.com/development/plugins/localization
Translate an Aikeedo plugin: wrap strings in gettext functions, extract catalogs with the console, and ship translations with your package.
Aikeedo uses gettext. Wrap your user-facing strings in the translation functions, ship `.po` catalogs inside your package, and Aikeedo merges them into the dictionary for every request.
## Wrap your strings
The functions are global, so they work in PHP, and they're registered as Twig functions and filters too.
| Function | Use for |
| - | - |
| `__('Save')` | A plain string |
| `p__('button', 'Save')` | A string with a context, so translators can tell "Save" the button from "Save" the verb |
| `n__('%d note', '%d notes', $count)` | Plurals |
| `noop__('Later')` | Marking a string for extraction without translating it yet |
```php theme={null}
throw new HttpException(__('The Acme API key is missing.'));
```
```twig theme={null}
```
In JavaScript that ships with your plugin, the same `__()` helper is available on pages that load Aikeedo's bundles.
Use a context for every short UI string. `p__('button', 'Open')` and `p__('label', 'Open')` can be translated differently, which matters in languages where verbs and adjectives don't share a form.
## Catalog layout
```text theme={null}
extra/extensions/acme/hello/
└── locale/
├── de-DE/LC_MESSAGES/messages.po
├── es-ES/LC_MESSAGES/messages.po
└── fr-FR/LC_MESSAGES/messages.po
```
* One directory per locale, named with the `xx-XX` code Aikeedo uses.
* The file is always `messages.po`.
* Set the `X-Domain` header to `messages`, so your strings merge into the default dictionary and plain `__()` calls resolve. Without that header the catalog is loaded under the `plugin` domain, and your `__()` calls won't find their translations.
```po locale/de-DE/LC_MESSAGES/messages.po theme={null}
msgid ""
msgstr ""
"Language: de-DE\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Domain: messages\n"
msgctxt "heading"
msgid "Notes"
msgstr "Notizen"
```
Aikeedo loads catalogs for every installed plugin, active or not, for the language it resolved for the request.
## Extract strings
Run the console command from the installation root, and point it at your package:
```bash theme={null}
php bin/console app:locale:extract --target=acme/hello
```
It scans your `.twig`, `.php` and `.js` files, creates or updates the catalogs, removes entries that no longer exist in the source, and rebuilds the browser catalogs.
| Option | Purpose |
| - | - |
| `--target`, `-t` | `core` or a package name; repeat for several targets |
| `--all`, `-a` | The core application and every package |
| `--path`, `-p` | Scan a directory instead, such as a checkout outside the installation |
| `--locale-dir` | Where `--path` writes catalogs, defaulting to `/locale` |
| `--domain` | Gettext domain for `--path`, read from `composer.json` by default |
| `--language`, `-l` | Restrict to specific languages |
| `--no-prune` | Keep entries that no longer appear in the source |
| `--no-build` | Skip rebuilding the browser catalogs |
Developing outside the installation directory works the same way:
```bash theme={null}
php bin/console app:locale:extract --path=../acme-hello
```
## Translate
Aikeedo can machine-translate the missing entries with a model:
```bash theme={null}
php bin/console app:locale:translate --target=acme/hello --all
```
| Option | Purpose |
| - | - |
| `--lang`, `-l` | Translate specific languages |
| `--all`, `-a` | Every language that has a catalog |
| `--target`, `-t` / `--path`, `-p` | Same targeting as the extract command |
| `--force`, `-f` | Retranslate entries that already have a translation |
| `--dry-run` | Show what would change |
Review machine translations before release, especially short UI strings where context matters.
## Rebuild browser catalogs
Strings used from JavaScript are compiled into per-language files served to the browser:
```bash theme={null}
php bin/console app:locale:build
```
Extracting and translating already do this, and clearing the cache from the admin panel rebuilds them too.
## Ship the catalogs
Include the `locale/` directory in your release archive. Nothing generates translations at install time, so a package without catalogs shows English to everyone.
## Checklist
Every user-visible string goes through `__()`, `p__()` or `n__()`.
Navigation labels use the `nav` context.
Catalogs live at `locale/{code}/LC_MESSAGES/messages.po`.
Every catalog sets `X-Domain: messages`.
You re-ran extraction after the last wording change.
The `locale/` directory is in your release archive.
## Related
* [Localization settings](/advanced/localization)
* [Theme localization](/development/themes/localization)
* [Localization internals](/development/core/localization-internals)
# Plugin manifest reference
Source: https://docs.aikeedo.com/development/plugins/manifest
Every composer.json field Aikeedo reads from a plugin or theme package, with validation rules and examples.
A plugin's `composer.json` is both a Composer manifest and Aikeedo's metadata. `Plugin\Domain\Context` parses it at boot, and the `heyaikeedo/composer` installer reads it during installation.
## Complete example
```json composer.json theme={null}
{
"name": "acme/hello",
"description": "A starter plugin for Aikeedo",
"version": "1.2.0",
"type": "aikeedo-plugin",
"homepage": "https://acme.test",
"time": "2026-09-15",
"license": "proprietary",
"authors": [
{
"name": "Acme",
"email": "dev@acme.test",
"homepage": "https://acme.test",
"role": "Owner"
}
],
"support": {
"docs": "https://docs.acme.test",
"email": "support@acme.test",
"issues": "https://github.com/acme/hello/issues",
"source": "https://github.com/acme/hello"
},
"require": {
"heyaikeedo/composer": "^1.0.0"
},
"extra": {
"entry-class": "Acme\\Hello\\Plugin",
"title": "Hello",
"tagline": "Say hello to your users",
"description": "A starter plugin for Aikeedo",
"logo": "https://acme.test/logo.svg",
"icon": "https://acme.test/icon.svg",
"default_url": "/admin/settings/hello",
"compatibility": ">=5.0.0",
"status": "inactive",
"public": ["assets"]
},
"autoload": {
"psr-4": {
"Acme\\Hello\\": "src/"
}
}
}
```
## Required fields
A package that breaks any of these rules is rejected with `InvalidPluginComposerJsonFileException` and never boots.
Either `aikeedo-plugin` or `aikeedo-theme`. It decides where the installer puts the package and how Aikeedo treats it.
The Composer package name, `vendor/package`. **It must match the installation directory**, `extra/extensions/{vendor}/{package}`.
Every plugin and theme must require the installer package, normally `"^1.0.0"`. Use `"^1.2.0"` if you rely on object entries in `extra.public`.
Fully qualified name of the class that implements `Plugin\Domain\PluginInterface`. Escape backslashes in JSON: `"Acme\\Hello\\Plugin"`. The legacy spelling `extra.entry_class` is still accepted; prefer `entry-class`. Required for plugins, optional for themes.
Maps your namespace to `src/`, so Composer can load the entry class. Required whenever the package ships PHP.
## Metadata
Semantic version, for example `1.2.0`. Installing from an archive runs `composer require :`, so keep it accurate and raise it with every release.
Shown in the admin plugins list. Aikeedo reads the **top-level** `description`, not `extra.description`.
Your product or documentation URL.
Release date, for example `2026-09-15`, parsed as the package's release timestamp.
SPDX identifier or your own license name.
Each entry may have `name`, `email`, `homepage` and `role`.
Any of `chat`, `docs`, `email`, `forum`, `irc`, `issues`, `rss`, `source` and `wiki`. They're shown as support links.
## The `extra` block
Display name in the admin panel. Falls back to the package name.
Short one-line summary.
URL of a logo image.
URL of an icon image.
Where the plugin's name links to in the admin plugins list, for example `/admin/settings/hello`. The link appears only while the plugin is active. Point it at your settings page.
`active`, `inactive` or `failed`. **Aikeedo owns this value**: installing sets it to `inactive`, and activating or deactivating writes the new value back into this file. Ship your package with `inactive`.
Files and directories to publish to the web root. See [Public assets](/development/plugins/public-assets) for the entry formats.
The Aikeedo versions your plugin supports, for example `">=5.0.0"`. This is informational: the application doesn't enforce it, so check the version yourself if your code depends on it.
## Dependencies
Declare third-party libraries in `require` as usual:
```json theme={null}
"require": {
"heyaikeedo/composer": "^1.0.0",
"league/flysystem-aws-s3-v3": "^3.0"
}
```
Installing the plugin runs Composer in the installation root, so your dependencies are resolved against the packages Aikeedo already ships.
Requiring a library that Aikeedo already depends on at an incompatible version makes the install fail. Keep constraints wide, and prefer libraries that are already present, such as `psr/http-client`, `firebase/php-jwt` and `league/flysystem`.
## Themes
Themes use the same file with two differences: `type` is `aikeedo-theme`, and `extra.entry-class` is optional, since a theme can be templates only. `extra.status` has no effect for themes, because the active theme is the one selected in the admin panel. See the [theme manifest](/development/themes/manifest).
## Validation checklist
The directory path matches `name`.
`type` is `aikeedo-plugin`.
`require` includes `heyaikeedo/composer`.
`extra.entry-class` points at a class that implements `PluginInterface`.
`autoload.psr-4` maps that namespace to `src/`.
`version` is set and increases with each release.
## Related
* [Lifecycle and hooks](/development/plugins/lifecycle-and-hooks)
* [Public assets](/development/plugins/public-assets)
* [Packaging and distribution](/development/plugins/packaging-and-distribution)
# Navigation
Source: https://docs.aikeedo.com/development/plugins/navigation
Add entries to the Aikeedo app and admin menus from a plugin, choose the right section, and control icons and badges.
Menus are built from a navigation registry that the core fills at boot. A plugin adds its own entries in `boot()`, so its pages appear alongside the built-in ones.
## Add an item
```php src/Plugin.php theme={null}
description = p__('nav', 'Capture and organize notes');
$item->isBuiltIn = false;
$this->nav->item('app.build', $item);
}
}
```
`Registry::item(string $target, Item $item)` appends to a section. If an item with the same URL is already registered in that section, the new one is ignored, so booting twice can't duplicate an entry.
## Item reference
```php theme={null}
new Item(string $url, string $label, ?string $icon = null)
```
| Property | Type | Purpose |
| - | - | - |
| `url` | `string` | Destination, for example `/app/notes` |
| `label` | `string` | Menu text. Wrap it in `p__('nav', '...')` |
| `icon` | `?string` | Icon, see the formats below |
| `description` | `?string` | Supporting text, shown in card-style lists such as Settings |
| `info` | `?string` | Extra hint text |
| `tags` | `array` | Free-form tags |
| `isExperimental` | `bool` | Adds an "experimental" marker |
| `isBuiltIn` | `bool` | Set it to `false` so the entry is marked as coming from a plugin |
| `from`, `to` | `?string` | Optional date range for temporary entries |
| `template` | `?string` | A Twig template to render instead of a link, for sections that support injected cards |
### Icon formats
| Value | Renders |
| - | - |
| `'notes'` | A [Tabler](https://tabler.io/icons) icon, the default |
| `'svg:path/to/icon.svg'` | An SVG file |
| `'include:@acme-notes/icon.twig'` | An included Twig template |
| `'src:https://...'` | An image URL |
## Sections
| Section | Where it appears |
| - | - |
| `app.primary` | Main app sidebar, top group |
| `app.build` | App sidebar, "Build" group |
| `app.engagement` | App sidebar, "Engagement" group |
| `app.apps` | App sidebar, "Apps" group |
| `app.settings_nav.account` | User settings sidebar, "Account" group |
| `app.settings_nav.workspace` | User settings sidebar, "Workspace" group |
| `app.settings_nav.affiliate` | User settings sidebar, "Affiliate" group, when affiliates are enabled |
| `admin.primary` | Admin sidebar, top group |
| `admin.ai` | Admin sidebar, "AI" group |
| `admin.billing` | Admin sidebar, "Billing" group |
| `admin.affiliates` | Admin sidebar, "Affiliates" group |
| `admin.accounts` | Admin sidebar, "Accounts" group |
| `admin.platform` | Admin sidebar, "Platform" group |
| `admin.settings.common` | Cards on the admin **Settings** index |
| `admin.settings.features` | Cards on **Settings → Features** |
Put an integration's configuration under `admin.settings.common`, and a feature that users interact with under `admin.settings.features`. Use the app sections for pages your users open every day.
Creating a section is possible too, though it only shows up where a template renders it:
```php theme={null}
$this->nav->section('app.apps', p__('nav', 'Apps'), true);
```
The third argument makes the group collapsible.
## Show items conditionally
Registration happens once per request, so read your feature flag in `boot()`:
```php theme={null}
use Application;
public function boot(Context $context): void
{
$adminItem = new Item('/admin/settings/features/notes', p__('nav', 'Notes'), 'notes');
$adminItem->description = p__('nav', 'Configure the notes feature');
$adminItem->isBuiltIn = false;
$adminItem->isExperimental = true;
$this->nav->item('admin.settings.features', $adminItem);
// User-facing entries only when the feature is turned on
if (Application::make('option.features.notes.is_enabled', false)) {
$item = new Item('/app/notes', p__('nav', 'Notes'), 'notes');
$item->isBuiltIn = false;
$this->nav->item('app.build', $item);
}
}
```
Keep the admin entry visible even when the feature is off, or administrators can't find the switch that turns it back on.
## Highlighting the current page
Templates decide which entry is active from the `active_menu` variable, so set it to the same URL you registered:
```twig theme={null}
{% set active_menu = '/app/notes' %}
```
Admin settings pages use `{% set active_menu = 'settings' %}` instead, and pages under **Plugins** use `{% set active_menu = '/admin/plugins' %}`.
## Translating labels
Labels pass through gettext with the `nav` context, and the templates translate them again at render time. Always wrap them:
```php theme={null}
$item = new Item('/app/notes', p__('nav', 'Notes'), 'notes');
```
Ship the catalogs with your plugin, as described in [Localization](/development/plugins/localization).
## Related
* [App pages and APIs](/development/plugins/app-pages-and-apis)
* [Admin settings pages](/development/plugins/admin-settings-pages)
* [Plugin quickstart](/development/plugins/quickstart)
# Aikeedo plugins
Source: https://docs.aikeedo.com/development/plugins/overview
How Aikeedo plugins are packaged, discovered and booted, what they can extend, and what they cannot do.
A plugin is a Composer package that Aikeedo loads at boot. It can add pages, API endpoints, settings screens, navigation items, scheduled work and integrations, and it can plug new implementations into the core's extension points, without you editing a single core file.
This page applies to Aikeedo 5.x. Class names, paths and signatures match the 5.0 source code.
## Anatomy of a plugin
```text theme={null}
extra/extensions/acme/hello/
├── composer.json # package metadata; type: aikeedo-plugin
├── src/
│ └── Plugin.php # entry class; implements PluginInterface
├── templates/ # Twig templates (any layout you like)
└── locale/ # translation catalogs
```
Two things make it a plugin:
1. `composer.json` declares `"type": "aikeedo-plugin"`, requires `heyaikeedo/composer`, and names an entry class in `extra.entry-class`.
2. The entry class implements `Plugin\Domain\PluginInterface`:
```php src/Plugin.php theme={null}
loader->addPath(__DIR__ . '/../templates', 'acme-hello');
}
}
```
The entry class is resolved from the dependency injection container, so **anything you type-hint in the constructor is injected**. That is how a plugin reaches the core: you ask for a registry, a collection or a mapper, and you register your own implementations inside `boot()`.
`Context` is read-only metadata about your package, built from its `composer.json`. It has no container or event dispatcher on it. Use constructor injection instead.
## How a plugin is loaded
```mermaid theme={null}
flowchart TD
A[Application boots] --> B[Core service providers and module bootstrappers]
B --> C[PluginModuleBootstrapper]
C --> D[PluginFinder scans extra/extensions for composer.json]
D --> E[Context built and validated per package]
E --> F{Active and healthy?}
F -- no --> G[Skipped]
F -- yes --> H[PluginLoader resolves the entry class from the container]
H --> I["boot(Context) registers routes, templates, services, listeners"]
I --> J[HTTP request is handled]
```
Key points:
* Plugins boot **after** every core module, so all core registries exist by the time `boot()` runs.
* `boot()` runs on every request, including console and cron runs. Keep it cheap: register things, don't do work.
* Only plugins whose status is `active` boot. Themes are a special case: a theme is active when it's the selected theme.
* If a plugin crashes while booting, Aikeedo records the failure in `var/plugin-health.json` and skips the plugin on later requests, so one broken plugin can't take the site down. With `DEBUG` on, the error is raised instead. See [Debugging](/development/plugins/debugging).
## What plugins can extend
| Capability | Where to start |
| - | - |
| Admin settings pages | [Admin settings pages](/development/plugins/admin-settings-pages) |
| Pages and JSON endpoints for signed-in users | [App pages and APIs](/development/plugins/app-pages-and-apis) |
| Public pages, forms, endpoints and webhooks | [Public pages and endpoints](/development/plugins/public-endpoints-and-embeds) |
| Menu entries in the app and admin panel | [Navigation](/development/plugins/navigation) |
| Reactions to domain events, and scheduled work | [Events and cron](/development/plugins/events-and-cron) |
| Payment gateways | [Payment gateways](/development/plugins/guides/payments/overview) |
| Tax engines | [Tax engines](/development/plugins/guides/tax-engine) |
| Storage backends | [Storage adapters](/development/plugins/guides/storage-adapter) |
| Vector databases | [Vector stores](/development/plugins/guides/vector-store) |
| AI providers and models | [AI providers](/development/plugins/guides/ai-provider) |
| Chat tools | [AI tools](/development/plugins/guides/ai-tool) |
| Currency exchange rates | [Currency rate providers](/development/plugins/guides/currency-rate-provider) |
| Per-plan settings | [Plan config extensions](/development/plugins/guides/plan-config-extension) |
| Conversation import sources | [Import adapters](/development/plugins/guides/import-adapter) |
| Your own extension points, for other plugins to implement | [Custom extension points](/development/plugins/custom-extension-points) |
## What plugins cannot do
| Limitation | Why | What to do instead |
| - | - | - |
| Add Doctrine entities | Doctrine maps only the core `src/` directory | Store data in options, reuse core entities, or use your own external store. See [Data and persistence](/development/plugins/data-and-persistence) |
| Add database migrations | Migration paths are fixed in `config/migrations.php` | Create any schema you need from an install hook against your own connection, or avoid extra tables |
| Add console commands | The command map in `config/commands.php` is static | Expose the action as an admin endpoint, or run it from cron |
| Replace core templates | Themes and plugins add namespaces, they don't override core paths | Use `resources/views/overrides/`. See [Extending Aikeedo](/development/extending-aikeedo) |
## Where plugins live
| Path | Contents |
| - | - |
| `extra/extensions/{vendor}/{name}` | The installed package, where Composer puts it |
| `extra/artifacts/` | Uploaded plugin archives, used as a Composer artifact repository |
| `public/e/{vendor}/{name}` | Public assets copied from `extra.public`. See [Public assets](/development/plugins/public-assets) |
| `var/plugin-backup/` | Backups taken before a reinstall |
| `var/plugin-health.json` | Recorded boot failures |
## Next steps
A working plugin with a settings page and a menu item, step by step.
Every `composer.json` field Aikeedo reads.
Install, activate, deactivate, uninstall and update.
The core services a plugin reaches through its constructor.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Packaging and distribution
Source: https://docs.aikeedo.com/development/plugins/packaging-and-distribution
Package an Aikeedo plugin as an installable archive, version it, and distribute it to customers.
Aikeedo installs plugins from a ZIP archive uploaded in the admin panel, or from any Composer repository. Both paths run Composer, so packaging is mostly about producing a clean, correctly versioned archive.
## Archive layout
`composer.json` must sit at the **root** of the archive, not inside a wrapper folder:
```text theme={null}
acme-hello.zip
├── composer.json
├── README.md
├── src/
│ ├── Plugin.php
│ └── SettingsRequestHandler.php
├── templates/
│ └── settings.twig
├── public/
│ └── assets/… # only if extra.public publishes them
└── locale/
└── de-DE/LC_MESSAGES/messages.po
```
Leave out development files: `.git`, `node_modules`, tests, build configuration, `.env` files and editor settings. Include build **output** if your plugin ships assets.
```bash theme={null}
cd acme-hello
zip -r ../acme-hello-1.2.0.zip . \
-x '.git/*' 'node_modules/*' 'tests/*' '.env*' '*.DS_Store'
```
## Versioning
* Use semantic versioning in `version`, and raise it for every release. Installing from an archive runs `composer require :`, so a stale version installs the wrong code.
* Record the Aikeedo versions you support in `extra.compatibility`, for example `">=5.0.0"`. Nothing enforces it, so check the version yourself if your code depends on a specific release.
* Ship with `extra.status` set to `inactive`. Aikeedo manages that field afterwards.
```json composer.json theme={null}
{
"name": "acme/hello",
"version": "1.2.0",
"type": "aikeedo-plugin",
"require": { "heyaikeedo/composer": "^1.0.0" },
"extra": {
"entry-class": "Acme\\Hello\\Plugin",
"title": "Hello",
"default_url": "/admin/settings/hello",
"compatibility": ">=5.0.0",
"status": "inactive"
},
"autoload": { "psr-4": { "Acme\\Hello\\": "src/" } }
}
```
## Dependencies
Declare third-party libraries in `require`. They're installed into the installation's own `vendor/` directory when the plugin is installed, so don't vendor them into your archive.
Your constraints are resolved against everything Aikeedo already requires. A narrow constraint on a shared library can make installation fail. Keep ranges wide, and prefer libraries the application already ships, such as `psr/http-client`, `firebase/php-jwt`, `league/flysystem` and `ramsey/uuid`.
## Install paths
| How | What happens |
| - | - |
| **Admin upload** | The archive is validated, stored in `extra/artifacts/`, then installed with Composer |
| **`composer require acme/hello`** | Resolved from a path repository, a private Composer repository, or Packagist |
| **Aikeedo update** | Every installed plugin is reinstalled so autoloaders and public assets are rebuilt |
Installing over an existing copy backs the old directory up first and restores it if anything fails.
## Ship a README
Customers read this before they read your code. Cover:
* What the plugin does, and which Aikeedo versions it supports
* Installation steps, including anything to configure first, such as API keys
* Where its settings live in the admin panel
* Required third-party accounts, and any cost involved
* Whether it needs cron, outbound network access, or a webhook URL the provider must reach
* A changelog
## Release checklist
`version` is raised, and the changelog is updated.
`composer.json` sits at the archive root, and the archive has no wrapper folder.
Development files are excluded, and build output is included.
Translation catalogs are up to date and included.
A clean installation test passes: upload, activate, configure, use, deactivate, uninstall.
No errors in `var/log` with `DEBUG=true`.
Secrets, license keys and customer data are nowhere in the archive.
Nothing in the core is modified, so an Aikeedo update can't overwrite your work or break it.
Any public route follows the [security checklist](/development/plugins/public-endpoints-and-embeds#security-checklist).
## Distribution options
| Option | Notes |
| - | - |
| **Direct download** | Give customers the ZIP; they upload it in the admin panel. Simplest, but updates are manual. |
| **Private Composer repository** | Customers add the repository and run `composer require`. Good for teams that manage installations from the CLI. |
| **Packagist** | Only for plugins you're happy to publish publicly. |
Whatever you choose, keep older archives available: customers on an older Aikeedo release may need the matching version of your plugin.
We're looking for community-built plugins to list on aikeedo.com, linking to your own page. See what to send us.
## Related
* [Manifest reference](/development/plugins/manifest)
* [Lifecycle and hooks](/development/plugins/lifecycle-and-hooks)
* [Public assets](/development/plugins/public-assets)
* [Debugging](/development/plugins/debugging)
# Public assets
Source: https://docs.aikeedo.com/development/plugins/public-assets
Publish CSS, JavaScript, images and loader scripts from an Aikeedo plugin to the web root with the extra.public manifest entry.
Files inside `extra/extensions/` aren't web-accessible. To serve a stylesheet, script or image, list it in `extra.public`, and the installer copies it into the web root when the plugin is installed or updated.
## Declare files to publish
```json composer.json theme={null}
{
"extra": {
"public": [
{ "source": "public/assets/*", "target": "." },
{ "source": "widget/dist/loader.js", "target": "/acme-feedback.js" }
]
}
}
```
By default, files land in `{web root}/e/{vendor}/{package}/`, which is served from `/e/{vendor}/{package}/...`.
## Entry formats
| Entry | Copied to | Served from |
| - | - | - |
| `"assets/app.css"` (plain string) | `public/e/acme/hello/assets/app.css` | `/e/acme/hello/assets/app.css` |
| `{ "source": "dist" }` | `public/e/acme/hello/dist` | `/e/acme/hello/dist/...` |
| `{ "source": "dist", "target": "." }` | `public/e/acme/hello/dist` | `/e/acme/hello/dist/...` |
| `{ "source": "dist", "target": "assets" }` | `public/e/acme/hello/assets` | `/e/acme/hello/assets/...` |
| `{ "source": "dist/*", "target": "." }` | The contents of `dist` into `public/e/acme/hello/` | `/e/acme/hello/...` |
| `{ "source": "widget/dist/loader.js", "target": "/loader.js" }` | `public/loader.js` | `/loader.js` |
| `{ "source": "dist", "target": "/." }` | `public/dist` | `/dist/...` |
A plain string keeps the whole source path, which is why the newer object form is clearer. A `target` that starts with `/` is resolved against the web root, so use it sparingly: that's a shared namespace, and a generic name like `/sdk.js` can collide with another plugin.
The object form requires a recent `heyaikeedo/composer`. Require `"^1.2.0"` when you use `{ "source": ..., "target": ... }` entries, and `"^1.0.0"` when plain strings are enough.
## When files are copied
| Composer event | What happens |
| - | - |
| Package installed | Files are copied, and the copy is recorded in `vendor/aikeedo-file-mappings.json` |
| Package updated | Recorded files are removed, then copied again |
| Package uninstalled | Recorded files are removed, and the now-empty `e/{vendor}/{package}` directory is cleaned up |
The web root comes from the `PUBLIC_DIR` environment variable, so installations that serve from `public_html` work without any change on your side.
Only files that existed when the package was installed are copied. Editing a file during development doesn't republish it: run `composer require acme/hello` again, or copy it yourself while iterating.
## Reference published files
```twig theme={null}
```
For a build with hashed filenames, publish the Vite manifest along with the bundle and resolve the name in PHP:
```php theme={null}
$manifest = json_decode(
(string) file_get_contents(dirname(__DIR__) . '/public/assets/.vite/manifest.json'),
true
);
$file = $manifest['assets/src/index.js']['file'] ?? null;
$url = '/e/acme/hello/' . $file;
```
Hashed filenames from an older build aren't deleted when you publish a new one, because the mapping records what the current install copied. Clean stale files out of your build directory before packaging a release.
## Development workflow
Point an environment variable at your dev server, and fall back to the published path:
```twig theme={null}
{% set base = env.ACME_HELLO_ASSETS_SERVER|default('/e/acme/hello') %}
```
```ini .env theme={null}
ACME_HELLO_ASSETS_SERVER=http://localhost:5175
```
Remember that `env` exposes the whole environment to templates, so read only the key you need and never dump the object.
## What not to publish
* Source files, tests and configuration. Publish build output only.
* Anything containing credentials. Published files are world-readable.
* Large media that would be better served from the CDN. See [Files and storage](/development/plugins/files-and-storage).
## Related
* [Manifest reference](/development/plugins/manifest)
* [Frontend integration](/development/plugins/frontend-integration)
* [Packaging and distribution](/development/plugins/packaging-and-distribution)
# Public pages and endpoints
Source: https://docs.aikeedo.com/development/plugins/public-endpoints-and-embeds
Add Aikeedo plugin routes that visitors can reach without signing in: public pages, forms, JSON endpoints and webhook receivers, and how to keep them safe.
Not every route a plugin adds is for signed-in users. A landing page, a contact form, a status endpoint or a webhook receiver has to answer anyone who asks. Those routes skip the authorization step, so the checks it normally provides become your responsibility.
Read the [security checklist](#security-checklist) before you ship a public route. Most serious plugin vulnerabilities come from public routes that trust their input.
## What makes a route public
Authentication is applied by `AuthorizationMiddleware`, and only the base classes for signed-in areas add it. A handler is public when neither it nor its base class carries that middleware.
| Base class | Signed-in user required | Use for |
| - | - | - |
| `Presentation\RequestHandlers\AbstractRequestHandler` | No | Public pages, forms and JSON endpoints |
| No base class | No | Webhook receivers and other routes that need a minimal stack |
| `…\App\AppView`, `…\Api\Api` | Yes | [App pages and APIs](/development/plugins/app-pages-and-apis) |
| `…\Admin\AbstractAdminRequestHandler` and its view variant | Yes, admin only | [Admin settings pages](/development/plugins/admin-settings-pages) |
`AbstractRequestHandler` is the right starting point for almost every public route. It's what the core's login, signup and policy pages extend, and it adds:
| Middleware | What it does |
| - | - |
| `ExceptionMiddleware` | Turns exceptions into responses. See [Errors](#errors). |
| `InstallMiddleware` | Redirects to the installer until Aikeedo is installed |
| `RequestBodyParserMiddleware` | Decodes JSON and form bodies into a `stdClass` |
| `UserMiddleware` | Attaches the signed-in user when there is one, and does nothing otherwise |
| `LocaleMiddleware` | Resolves the locale and loads translations |
## A public page
Add `ViewMiddleware` to render a Twig template:
```php src/RequestHandlers/PricingView.php theme={null}
getAttribute(UserEntity::class);
return new ViewResponse('@acme-pricing/templates/compare.twig', [
'is_signed_in' => $user !== null,
]);
}
}
```
| Piece | Why |
| - | - |
| `ViewMiddleware` | Renders the `ViewResponse` and adds the template globals, such as `option`, `user` and `locale` |
| `[locale:locale]?` | Makes the page work under a language prefix such as `/de-DE/compare` |
| `UserEntity` attribute | Set when a signed-in user visits, `null` otherwise. Use it to adapt the page, never to protect it. |
| `@acme-pricing/...` | Your plugin's template namespace, registered in `boot()`. See [Dependency injection and services](/development/plugins/dependency-injection-and-services). |
The template can extend the same minimal layout the core's public pages use, so it works with any theme:
```twig templates/compare.twig theme={null}
{% extends "/layouts/minimal.twig" %}
{% block title p__('title', 'Compare plans') %}
{% block template %}
{{ p__('heading', 'Compare plans') }}
{% if not is_signed_in %}
{{ p__('button', 'Get started') }}
{% endif %}
{% endblock %}
```
To build a page into the active theme's own design instead, see [Theme custom pages](/development/themes/custom-pages).
## A public form or JSON endpoint
A form posts to a handler of its own. Validate everything, and add `CaptchaMiddleware` to anything that creates records or sends email:
```php src/RequestHandlers/SubmitInquiry.php theme={null}
getParsedBody();
$email = $body->email ?? null;
if (!is_string($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new ValidationException('Enter a valid email address', 'email');
}
$this->dispatcher->dispatch(new CreateInquiryCommand(
email: $email,
ip: $request->getAttribute(ClientIpMiddleware::class),
));
return new JsonResponse(['ok' => true], StatusCode::CREATED);
}
}
```
| Middleware | Why you'd add it |
| - | - |
| `CaptchaMiddleware` | When reCAPTCHA is enabled in the admin panel, rejects requests without a valid `captcha-token` body field. When it's disabled, it lets every request through. |
| `ClientIpMiddleware` | Resolves the visitor's IP behind proxies and stores it on the request under `ClientIpMiddleware::class` |
To send a captcha token, include the core snippet inside your form. It renders nothing when reCAPTCHA is off, and otherwise fills a `captcha-token` field for you:
```twig theme={null}
```
Keep public endpoints outside `/api` and `/admin`, which belong to the core. Under `/api` and `/admin/api`, `UserMiddleware` also ignores the session cookie and only accepts an API key or a bearer token.
## Errors
`ExceptionMiddleware` decides what a failed request returns:
| Thrown | Response |
| - | - |
| `Presentation\Validation\ValidationException` | `400` JSON with `message` and `param` |
| `Presentation\Exceptions\HttpException` and its subclasses, such as `NotFoundException` | JSON with the exception's status code |
| `UnauthorizedException` | JSON under `/api` and `/admin/api`. Anywhere else, a redirect to `/login`. |
| Anything else | Rethrown when `DEBUG=true`. Otherwise it's logged, and the response is an empty `500`, or a JSON body with a correlation ID under `/api` and `/admin/api`. |
A public page that should show "not found" can return a `ViewResponse` with `StatusCode::NOT_FOUND` rather than throw, since a thrown `NotFoundException` becomes JSON.
## Routes without a base class
A handler that extends no base class gets no middleware at all: no error handling, no body parsing, no user and no locale. Use it when the standard stack gets in the way, such as for a webhook receiver that must read the raw body. Then add back only what you need, and always start with `ExceptionMiddleware`:
```php theme={null}
#[Middleware(ExceptionMiddleware::class)]
#[Route(path: '/acme-pricing/webhooks/crm', method: RequestMethod::POST)]
class CrmWebhook implements RequestHandlerInterface
```
## Receiving webhooks
Providers post to an endpoint you publish, so verify every request:
```php theme={null}
$signature = $request->getHeaderLine('X-Acme-Signature');
$body = (string) $request->getBody();
if (!$this->secret) {
// Fail closed: an unconfigured secret means you cannot verify anything.
throw new HttpException('Webhook secret is not configured', StatusCode::BAD_REQUEST);
}
$expected = hash_hmac('sha256', $body, $this->secret);
if (!hash_equals($expected, $signature)) {
throw new HttpException('Invalid signature', StatusCode::BAD_REQUEST);
}
```
Read the raw body for the signature, not the parsed one, and make handling idempotent: providers retry, and the same event can arrive twice.
Payment gateways have a dedicated webhook route and interface. See [Payment webhooks](/development/plugins/guides/payments/webhooks).
## Identifying visitors who aren't users
Some public features need to recognize the same visitor across requests without an Aikeedo account, such as a guest's saved progress. Aikeedo has no built-in visitor identity for this, so issue your own token:
* Sign it with a secret your plugin owns, scope it to your feature, and give it a short lifetime. The app ships `firebase/php-jwt` if you want JWTs.
* Verify it in your own middleware, and attach what it proves to the request.
* Only issue a token for something the caller proved. Creating a fresh identity is safe; returning a token for an identifier the caller merely supplied lets anyone impersonate anyone.
## Embeds
A page meant to be framed on other sites, such as a chat or feedback widget, is a public page with a few extra requirements: a loader script, a `frame-ancestors` policy, `postMessage` between the frames, and per-site visitor tokens. The [embeddable widget guide](/development/plugins/guides/embeddable-widget) walks through all of it.
## Security checklist
**Validate every input.** Public bodies, query strings and route parameters are attacker-controlled. Check types, lengths and formats before using them.
**Verify ownership of every ID.** When a route loads a record by ID, confirm the caller is allowed to see it. Loading by ID alone lets anyone read other people's data by guessing.
**Don't rely on the optional user for access.** A missing `UserEntity` on a public route is normal, not an error. If a route needs a user, it isn't public: extend an authenticated base class.
**Protect writes.** Add `CaptchaMiddleware` to forms and to anything that creates records or sends email.
**Limit abuse.** Aikeedo has no built-in rate limiter. Cap requests per IP in your plugin or at the web server, and reject oversized payloads early.
**Guard paid work.** Anything that spends AI credits must name the workspace that pays and check its balance first. Never let an anonymous request spend credits without an owner.
**Treat headers as hints.** `Origin`, `Referer` and custom headers can be forged by any client. Enforce access with tokens, signatures and server-side allowlists.
**Respond in constant shapes.** Don't reveal whether a record or email exists through different messages or timings.
**Return the minimum.** Never expose internal IDs, workspace details or configuration from a public route.
## CORS
Browsers only need CORS headers when another site's page calls your endpoint directly. Forms and pages on your own domain don't. When you do need them, reflect only origins you've allowlisted, for example from a plugin setting:
```php theme={null}
$origin = $request->getHeaderLine('Origin');
if (in_array($origin, $this->allowedOrigins, true)) {
$response = $response
->withHeader('Access-Control-Allow-Origin', $origin)
->withHeader('Vary', 'Origin');
}
```
Never reflect an arbitrary origin, and never combine `Access-Control-Allow-Origin: *` with credentials.
## Related
* [Routes and request handlers](/development/plugins/routes-and-request-handlers)
* [Authentication and authorization](/development/core/authentication-and-authorization)
* [Embeddable widget guide](/development/plugins/guides/embeddable-widget)
* [Theme custom pages](/development/themes/custom-pages)
* [Public assets](/development/plugins/public-assets)
# Build your first plugin
Source: https://docs.aikeedo.com/development/plugins/quickstart
Create, install and activate an Aikeedo plugin with an admin settings page, a saved option and a navigation entry.
In this guide you build `acme/hello`, a plugin that adds a settings page to the admin panel, stores a setting, and links to itself from the settings index. It takes about 15 minutes and touches every part of the plugin system you'll use again later.
## Prerequisites
* A local Aikeedo installation with `DEBUG=true` and `CACHE=false`. See [Local development](/development/local-development).
* Composer 2 and terminal access to the installation root.
* Administrator access to the admin panel.
## Step 1: Create the package
Plugins live in `extra/extensions/{vendor}/{name}`, and the directory path must match the package name.
```bash theme={null}
mkdir -p extra/extensions/acme/hello/src
mkdir -p extra/extensions/acme/hello/templates
```
## Step 2: Write the manifest
```json extra/extensions/acme/hello/composer.json theme={null}
{
"name": "acme/hello",
"description": "A starter plugin for Aikeedo",
"version": "1.0.0",
"type": "aikeedo-plugin",
"license": "proprietary",
"authors": [
{
"name": "Acme",
"email": "dev@acme.test"
}
],
"require": {
"heyaikeedo/composer": "^1.0.0"
},
"extra": {
"entry-class": "Acme\\Hello\\Plugin",
"title": "Hello",
"description": "A starter plugin for Aikeedo",
"default_url": "/admin/settings/hello",
"status": "inactive"
},
"autoload": {
"psr-4": {
"Acme\\Hello\\": "src/"
}
}
}
```
| Field | Why it matters |
| - | - |
| `name` | Must match the directory path, `acme/hello` |
| `type` | `aikeedo-plugin` tells the installer where to put the package |
| `version` | Composer needs it when the plugin is installed from an archive |
| `require` | `heyaikeedo/composer` provides the installer; it's mandatory |
| `extra.entry-class` | The class Aikeedo boots |
| `extra.default_url` | Where the admin plugins list links when the plugin is active |
See the [manifest reference](/development/plugins/manifest) for every supported field.
## Step 3: Write the entry class
The entry class is resolved from the container, so declare the core services you need as constructor parameters.
```php extra/extensions/acme/hello/src/Plugin.php theme={null}
.twig
$this->loader->addPath(__DIR__ . '/../templates', 'acme-hello');
// Scan this directory for request handlers with #[Route] attributes
$this->mapper->addPath(__DIR__);
// Link the settings page from Settings in the admin panel
$item = new Item(
'/admin/settings/hello',
p__('nav', 'Hello'),
'confetti'
);
$item->description = p__('nav', 'Settings for the Hello plugin');
$item->isBuiltIn = false;
$this->nav->item('admin.settings.common', $item);
}
}
```
`p__('nav', '...')` is a gettext call with a context. Wrapping strings this way lets you translate the plugin later. See [Localization](/development/plugins/localization).
## Step 4: Add a settings page
Routes are declared with attributes on a request handler. Extending `AbstractAdminViewRequestHandler` puts the route under `/admin` and applies the admin middleware, so only administrators reach it.
```php extra/extensions/acme/hello/src/SettingsRequestHandler.php theme={null}
$this->greeting ?? 'Hello',
]);
}
}
```
The `#[Inject('option.hello.greeting')]` attribute reads a stored option by dot path. It resolves to `null` until the option exists.
## Step 5: Add the template
Admin pages extend the core layout. Setting `xdata` to `settings` binds the page to Aikeedo's settings component, which submits the form to the options API for you.
```twig extra/extensions/acme/hello/templates/settings.twig theme={null}
{% extends "/layouts/main.twig" %}
{% set active_menu = 'settings' %}
{% set xdata = 'settings' %}
{% block title p__('title', 'Hello settings')|title %}
{% block template %}
{% endblock %}
```
The field name `hello[greeting]` decides where the value is stored: the top-level key becomes the option `hello`, and the value is merged into its JSON, so the template and your handler both read it as `option.hello.greeting`.
## Step 6: Install the plugin
The installation's `composer.json` already treats `extra/extensions/*/*` as a path repository, so Composer finds your package by name. From the installation root:
```bash theme={null}
composer require acme/hello
```
Composer registers the package's autoloader, which is what makes `Acme\Hello\Plugin` loadable.
## Step 7: Activate and verify
In the admin panel, open **Plugins**, find **Hello**, and activate it. Activation writes `extra.status` back to your `composer.json` and clears the cache.
Go to **Settings**. Your entry appears in the list, marked as a plugin addition. Open it, or go straight to `/admin/settings/hello`.
Type a greeting and select **Save changes**. A toast confirms the save.
Reload the page. The input keeps its value, which means the option was written and injected back into the handler.
Your plugin registers templates, a route, a menu entry and a setting.
## Step 8: Use the setting
Anywhere else in your plugin, read the option the same way:
```php theme={null}
use Easy\Container\Attributes\Inject;
public function __construct(
#[Inject('option.hello.greeting')]
private ?string $greeting = null,
) {}
```
Or dispatch a command to write one:
```php theme={null}
use Option\Application\Commands\SaveOptionCommand;
use Shared\Infrastructure\CommandBus\Dispatcher;
$this->dispatcher->dispatch(
new SaveOptionCommand('hello', json_encode(['greeting' => 'Hi there']))
);
```
## Troubleshooting
Check that the directory path matches the package `name`, that `type` is `aikeedo-plugin`, and that `require` includes `heyaikeedo/composer`. An invalid manifest is skipped.
The package isn't registered with Composer's autoloader. Run `composer require acme/hello`, or `composer dump-autoload` if it's already required.
Routes are scanned from the directories you pass to `AttributeMapper::addPath()`, and the result is cached when `CACHE=true`. Set `CACHE=false`, or clear the cache from **Status → Clear cache**.
The namespace in `addPath(__DIR__ . '/../templates', 'acme-hello')` must match the `@acme-hello/...` reference, and `boot()` must have run, which means the plugin has to be active.
## Next steps
Base classes, middleware, responses and validation.
Forms, option storage and secrets.
React to domain events and run scheduled work.
Ship your plugin as an installable archive.
For a complete public example, see the [Currency Beacon plugin](https://github.com/heyaikeedo/plugins-currency-beacon).
# Routes and request handlers
Source: https://docs.aikeedo.com/development/plugins/routes-and-request-handlers
Declare routes from a plugin with PHP attributes, pick the right base class and middleware stack, validate input, and return JSON or rendered views.
Aikeedo discovers routes by scanning classes for attributes. A plugin adds its own directory to that scan, then declares handlers the same way the core does.
## Register your routes
Add the directory that contains your request handlers in `boot()`:
```php src/Plugin.php theme={null}
use Easy\Router\Mapper\AttributeMapper;
public function __construct(
private AttributeMapper $mapper,
) {}
public function boot(Context $context): void
{
$this->mapper->addPath(__DIR__);
}
```
The mapper scans recursively, so pointing it at your `src/` directory is usually enough. Point it at a subdirectory, such as `__DIR__ . '/RequestHandlers'`, if you want to limit the scan.
Route discovery is cached when `CACHE=true` and `DEBUG=false`. After adding or changing a route on a cached installation, clear the cache from **Status → Clear cache**.
## Anatomy of a handler
```php src/RequestHandlers/HelloRequestHandler.php theme={null}
A handler that extends no base class gets no middleware at all: no body parsing, no user resolution and no error handling. Only do that when you need a minimal stack, such as for a webhook receiver, and add middleware explicitly. For ordinary public pages and endpoints, extend `AbstractRequestHandler`. See [Public pages and endpoints](/development/plugins/public-endpoints-and-embeds).
## Route attributes
```php theme={null}
#[Path('/notes')] // class-level prefix, inherited
#[Route(path: '/', method: RequestMethod::GET)] // GET /notes
#[Route(path: '/[uuid:id]', method: RequestMethod::PATCH, priority: Priority::HIGH)]
#[Middleware(MyMiddleware::class)] // extra middleware for this handler
```
| Attribute | Purpose |
| - | - |
| `Easy\Router\Attributes\Route` | Declares `path`, `method` and optional `priority`. Repeat it to bind several routes to one handler. |
| `Easy\Router\Attributes\Path` | A path prefix for the class and everything that extends it. |
| `Easy\Router\Attributes\Middleware` | Adds PSR-15 middleware, outermost first. |
### Path syntax
| Pattern | Matches |
| - | - |
| `/notes` | An exact path |
| `/[:slug]` | Any single segment, exposed as the `slug` attribute |
| `/[uuid:id]` | A UUID segment |
| `/[images\|videos:type]` | One of the listed values |
| `/[locale:locale]?/welcome` | An optional locale prefix such as `/de-DE` |
Route parameters arrive as request attributes:
```php theme={null}
$id = $request->getAttribute('id');
```
Give a route `priority: Priority::HIGH` when it would otherwise be shadowed by a more generic pattern.
## Read the request
```php theme={null}
use Presentation\Validation\Validator;
use User\Domain\Entities\UserEntity;
use Workspace\Domain\Entities\WorkspaceEntity;
public function __construct(
private Validator $validator,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Authenticated user and workspace, set by UserMiddleware
$user = $request->getAttribute(UserEntity::class);
$workspace = $request->getAttribute(WorkspaceEntity::class);
// Validate the parsed body; throws ValidationException (400) on failure
$this->validator->validateRequest($request, [
'title' => 'required|string|max:255',
'category_id' => 'sometimes|uuid',
]);
$payload = $request->getParsedBody();
$title = $payload->title;
// Query string
$query = (object) $request->getQueryParams();
// Uploads
$file = $request->getUploadedFiles()['file'] ?? null;
}
```
`RequestBodyParserMiddleware` decodes JSON and form bodies into a `stdClass`, so use `property_exists()` before reading optional fields.
## Return a response
| Response | Use for |
| - | - |
| `Presentation\Response\JsonResponse` | JSON payloads, with an optional `StatusCode` |
| `Presentation\Response\ViewResponse` | A Twig template plus data, rendered by `ViewMiddleware` |
| `Presentation\Response\RedirectResponse` | Redirects |
| `Presentation\Response\EmptyResponse` | An empty body, for example `204` or after a webhook |
| `Presentation\Response\Response` | A PSR-7 response you build yourself, for example a stream |
```php theme={null}
use Easy\Http\Message\StatusCode;
use Presentation\Resources\ListResource;
use Presentation\Response\JsonResponse;
return new JsonResponse($resource, StatusCode::CREATED);
```
### Shape your JSON with resources
Return a `JsonSerializable` resource rather than raw arrays, so your API matches the rest of Aikeedo:
```php src/Presentation/Resources/NoteResource.php theme={null}
(string) $this->note->getId()->getValue(),
'object' => 'note',
'title' => $this->note->getTitle()->value,
'created_at' => new DateTimeResource($this->note->getCreatedAt()),
];
}
}
```
`Presentation\Resources\ListResource` wraps collections as `{"object": "list", "data": [...]}`, and `CountResource` wraps counts, which keeps your endpoints consistent with the core API.
## Errors
Throw, and let `ExceptionMiddleware` translate the exception into a response:
| Exception | Result |
| - | - |
| `Presentation\Validation\ValidationException` | `400` with `code`, `message` and `param` |
| `Presentation\Exceptions\NotFoundException` | `404` |
| `Presentation\Exceptions\UnauthorizedException` | `401` for API paths, a redirect to `/login` for pages |
| `Presentation\Exceptions\HttpException` | The status code you pass; defaults to `422` |
| Anything else | `500`, logged with a correlation ID, or rethrown when `DEBUG` is on |
```php theme={null}
use Easy\Http\Message\StatusCode;
use Presentation\Exceptions\HttpException;
use Presentation\Exceptions\NotFoundException;
throw new NotFoundException('Note not found');
throw new HttpException('Provider rejected the request', StatusCode::BAD_GATEWAY);
```
## Custom middleware
Middleware is a plain PSR-15 class, so you can autowire dependencies into it:
```php src/Middlewares/RequireFeatureMiddleware.php theme={null}
isEnabled) {
throw new NotFoundException();
}
return $handler->handle($request);
}
}
```
Attach it with `#[Middleware(RequireFeatureMiddleware::class)]` on the handler or on a base class of your own.
## Group routes with your own base class
```php src/RequestHandlers/NotesApi.php theme={null}
getAttribute('id'));
/** @var Generator $generator */
$generator = $this->dispatcher->dispatch($cmd);
$response = (new Response())
->withHeader('Content-Type', 'text/event-stream')
->withHeader('Cache-Control', 'no-cache')
->withHeader('Connection', 'keep-alive')
// Required so nginx doesn't buffer the whole response
->withHeader('X-Accel-Buffering', 'no');
return $response->withBody(
new CallbackStream($this->stream(...), $generator)
);
}
private function stream(Generator $generator): void
{
foreach ($generator as $chunk) {
$this->streamer->sendEvent('text-delta', ['delta' => $chunk]);
}
}
}
```
`Presentation\EventStream\Streamer` writes one event at a time and flushes:
| Method | Purpose |
| - | - |
| `open()` | Clears output buffering and stops if the client already disconnected |
| `sendEvent(string $event, string\|array\|JsonSerializable $data = null, ?string $id = null)` | Writes an `event:`, `data:` and `id:` block, then flushes |
| `close()` | Marks the stream finished |
| `stream(Generator $generator)` | Streams Aikeedo's own message parts, used by the chat endpoint |
Call `open()` before your first event if you send events yourself rather than through `stream()`.
## The wire format
```text theme={null}
event: text-delta
data: {"delta":"Once upon a time"}
id: 1726480000.1234
event: done
data: {"id":"0190a1b2-..."}
id: 1726480000.9876
```
Pick short event names, and keep each `data` payload to a single JSON object. Clients parse this with the browser's `EventSource`, or with a streaming `fetch` and an SSE parser when they need to POST.
## Handle the write side carefully
The entity manager is flushed after the response is emitted, which for a stream is after the last byte. If your generator creates records that the client will reference, inject `Doctrine\ORM\EntityManagerInterface` and call `flush()` before or during the stream.
A user who closes the tab shouldn't keep a provider call running and a workspace paying for it. `Streamer::open()` exits when `connection_aborted()` reports a dead connection; check it in long loops of your own too.
Once the headers are sent you can't change the status code. Catch exceptions inside the generator and emit an `error` event with a message the client can show.
Reserve credits before you start, then consume the real cost as usage arrives. See [AI models and credits](/development/plugins/ai-models-and-credits).
## Forwarding AI output
When you stream a model's answer, fold Aikeedo's stream parts into your own events:
```php theme={null}
use Ai\Domain\ValueObjects\Stream\ErrorPart;
use Ai\Domain\ValueObjects\Stream\TextDeltaPart;
use Ai\Domain\ValueObjects\Stream\UsagePart;
private function stream(Generator $generator): void
{
$this->streamer->open();
foreach ($generator as $part) {
if ($part instanceof TextDeltaPart) {
$this->streamer->sendEvent('text-delta', $part);
}
if ($part instanceof ErrorPart) {
$this->streamer->sendEvent('error', $part);
}
if ($part instanceof UsagePart) {
// Internal accounting; don't leak cost details to the client.
$cost = $part->cost;
}
}
$this->streamer->close();
}
```
Stream parts are `JsonSerializable`, so passing one straight to `sendEvent()` produces a payload shaped like `{"type": "...", ...}`.
## Consume the stream in the browser
```javascript theme={null}
const res = await fetch(`/api/notes/${id}/summary`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const blocks = buffer.split('\n\n');
buffer = blocks.pop() ?? '';
for (const block of blocks) {
const event = block.match(/^event: (.*)$/m)?.[1];
const data = block.match(/^data: (.*)$/m)?.[1];
if (event === 'text-delta') {
this.summary += JSON.parse(data).delta;
}
}
}
```
PHP's built-in development server handles one request at a time, so a stream blocks everything else. Test streaming against nginx with PHP-FPM, or a similar setup, before you ship.
## Deployment notes
* `X-Accel-Buffering: no` disables nginx buffering; other proxies and CDNs have their own switch.
* Check `output_buffering` and any compression module, since gzip on a stream can delay flushes.
* Keep `max_execution_time` in mind for long generations.
## Related
* [AI models and credits](/development/plugins/ai-models-and-credits)
* [App pages and APIs](/development/plugins/app-pages-and-apis)
* [REST API streaming](/development/api/overview)
* [Text streaming settings](/advanced/text-streaming)
# Upgrading plugins to Aikeedo 5.0
Source: https://docs.aikeedo.com/development/plugins/upgrading-to-v5
What changed in Aikeedo 5.0 for plugin developers, and how to update a plugin written for 4.x.
Aikeedo 5.0 removed several features and the interfaces that went with them. A plugin written for 4.x keeps installing, but it can fatal at boot if it resolves a class that no longer exists, which marks it as failed.
Test your plugin against 5.0 before your customers update. A plugin that references a removed class breaks on the first request after the update.
## Removed interfaces and classes
### `TitleServiceInterface` is gone
`Ai\Domain\Title\TitleServiceInterface` and the per-provider `TitleGeneratorService` classes were removed.
```php theme={null}
// Before, 4.x
$service = $this->factory->create(TitleServiceInterface::class, $model);
$title = $service->generateTitle($conversation);
```
Use the title service, which reads the globally configured model for you:
```php theme={null}
// After, 5.0
use Ai\Application\Services\Titler;
public function __construct(
private Titler $titler,
) {}
```
For a one-shot completion that isn't a title, resolve a text completion service instead:
```php theme={null}
use Ai\Domain\Completion\TextCompletionServiceInterface;
$service = $this->factory->create(TextCompletionServiceInterface::class, $model);
$response = $service->complete($model, $instructions, $input);
```
### The per-plan titler model is gone
`PlanConfig::$titler` no longer exists. The title model is a single global setting under **Settings → Features → Utilities**, and the title service reads it. Remove any plan configuration or UI your plugin had for it.
## Removed features
| Removed in 5.0 | What to do |
| - | - |
| The Coder tool | Drop any integration. Existing code documents became chat conversations during the update. |
| The Composer (music) tool | Drop any integration, including its AI/ML API usage. |
| The standalone Writer editor | Writer documents became conversations. The `writer.model` plan setting is unused. |
| `POST /api/ai/completions` | Call the conversations API instead. |
## Renamed capabilities
The `generate_image` plan tool became `generate_media`, which now covers images, video, voice-over and transcription. Update any plan configuration your plugin reads or writes, and any capability check keyed on the old name.
## Translations
Every bundled plugin is translated in 5.0. If your plugin ships user-facing strings, wrap them in the gettext functions and ship catalogs, or your plugin will be the only untranslated part of the interface.
```bash theme={null}
php bin/console app:locale:extract --target=acme/hello
php bin/console app:locale:translate --target=acme/hello --all
```
See [Localization](/development/plugins/localization).
## Migration checklist
```bash theme={null}
grep -rn "TitleServiceInterface\|TitleGeneratorService\|generate_image\|ai/completions" src/
```
Replace title generation with `Titler`, completions with `TextCompletionServiceInterface`, and `generate_image` with `generate_media`.
Install the plugin on a 5.0 installation with `DEBUG=true`, activate it, exercise every page and endpoint, and check `var/log` for errors.
Extract and translate your strings, and include `locale/` in the archive.
Raise `version`, set `extra.compatibility` to `">=5.0.0"`, and tell customers to update the plugin together with Aikeedo.
The plugin boots on 5.0 with no entries in `var/plugin-health.json`.
## Related
* [Release notes](/versioning/release-notes)
* [How to update](/versioning/how-to-update)
* [Debugging](/development/plugins/debugging)
* [AI models and credits](/development/plugins/ai-models-and-credits)
# Build pipeline and assets
Source: https://docs.aikeedo.com/development/themes/build-and-assets
How the Aikeedo theme build works, how asset URLs are resolved, and how to run hot reloading during development.
The starter kit builds CSS and JavaScript with Vite, copies the package files into the installed theme, and publishes the result to the web root. Understanding that chain makes asset problems easy to diagnose.
## The pipeline
```mermaid theme={null}
flowchart LR
A["src/css, src/js"] -->|vite build| B[assets/ + .vite/manifest.json]
C["static/"] -->|copied as-is| D[BUILD_DIR]
B --> D
D -->|composer install| E["extra/extensions/{vendor}/{name}"]
E -->|extra.public| F["public/e/{vendor}/{name}"]
F -->|asset_url| G[Browser]
```
## Configuration
Two environment variables drive it:
| Variable | Meaning |
| - | - |
| `BUILD_DIR` | Where the build writes. Point it at the installed theme directory, `extra/extensions/{vendor}/{name}`. |
| `AIKEEDO_SERVER` | The Aikeedo origin the dev server proxies to, such as `http://localhost:8000` |
The starter commits a `.env` with defaults. Put your machine's paths in `.env.local`, which is ignored by git.
The production build empties `BUILD_DIR` first. Never point it at the installation root, the web root, or any directory that holds files you didn't build.
## Development
```bash theme={null}
npm run dev
```
The dev server:
* Serves on port **5174**, with a fixed port so `THEME_ASSETS_SERVER` stays valid.
* Proxies everything except its own paths to `AIKEEDO_SERVER`, so you browse the real site through Vite.
* Serves `/assets/...` from your local `static/` when the file exists, and proxies it otherwise.
* Copies `static/**` into `BUILD_DIR` continuously, so template changes reach the installation.
* Full-reloads the page when a `.twig` or a `.po` file changes.
On the Aikeedo side, set `THEME_ASSETS_SERVER` to the dev server:
```ini .env theme={null}
THEME_ASSETS_SERVER=http://localhost:5174
```
CSS updates apply without a reload. JavaScript and template changes trigger a full reload.
## Production build
```bash theme={null}
npm run build
```
This writes the compiled bundles into `BUILD_DIR/assets/` with hashed filenames, copies `static/` alongside them, and writes `BUILD_DIR/.vite/manifest.json` mapping source paths to built files.
Remove `THEME_ASSETS_SERVER` from the Aikeedo `.env` afterwards, or the site keeps looking for a dev server that isn't running.
## How asset URLs resolve
`asset_url` is the filter every theme template uses:
```twig theme={null}
```
It resolves in this order:
`{{ 'https://cdn.example.com/x.png' | asset_url }}` stays as it is.
The path is appended to that server, which is what makes hot reloading work.
A path listed in `public/e/{theme}/.vite/manifest.json`, such as `src/css/index.css`, becomes the hashed built file under `/e/{theme}/`.
The path is prefixed with `assets/` if needed and served from `/e/{theme}/assets/...`, with the application version as a cache-busting query.
`asset_url` is the theme filter. The similar `asset` filter resolves the **application's** own bundles and isn't for theme files.
## Publishing files to the web root
The manifest decides what reaches the browser:
```json composer.json theme={null}
{
"extra": {
"public": ["assets", ".vite"]
}
}
```
Those entries are copied to `public/e/{vendor}/{name}/` when the theme is installed or updated. A theme that publishes nothing has no CSS or JavaScript in production, even though the files exist in `extra/extensions/`.
## Styling
The starter uses Tailwind CSS 3 with CSS variables, so colors can change per mode:
```javascript tailwind.config.js theme={null}
export default {
content: ['!./node_modules/**/*', './**/*.twig', './src/**/*.{js,css}'],
darkMode: ['class', '[data-mode="dark"]'],
theme: {
extend: {
colors: {
main: 'rgb(var(--color-main) / )',
content: 'rgb(var(--color-content) / )',
},
},
},
};
```
The variables themselves are defined in a snippet, which is also where you switch them for dark mode. See [Dark mode and colors](/development/themes/dark-mode-and-colors).
Themes run their own Tailwind build, independent of the application's. The application uses Tailwind 4 and Vite 8; the starter uses Tailwind 3 and Vite 5. Your theme's versions are yours to choose, as long as the output is plain CSS and JavaScript.
## Packaging
| Command | Result |
| - | - |
| `npm run dev` | Dev server with hot reloading |
| `npm run build` | Production build into `BUILD_DIR` |
| `npm run serve` | Preview the production build |
| `npm run pack` | `theme.zip`, installable through the admin panel |
| `npm run release` | A versioned archive containing `theme.zip` plus `release/` |
See [Packaging and publishing](/development/themes/packaging-and-publishing).
## Troubleshooting
`extra.public` must include `assets` and `.vite`, and the theme must have been installed or reinstalled after the build so the files were copied.
`THEME_ASSETS_SERVER` is still set in the Aikeedo `.env`. Remove it outside development.
Either the dev server isn't copying to `BUILD_DIR`, or the Twig cache is on. Check `BUILD_DIR`, and set `CACHE=false`.
`emptyOutDir` clears `BUILD_DIR` on every build. Keep it pointed at the theme directory only.
## Related
* [Theme structure](/development/themes/structure)
* [Templates and layouts](/development/themes/templates-and-layouts)
* [Packaging and publishing](/development/themes/packaging-and-publishing)
# Custom pages
Source: https://docs.aikeedo.com/development/themes/custom-pages
Add public pages to an Aikeedo theme with a PHP entry class, attribute routes and your own templates.
A theme isn't limited to the landing page. With a small entry class it can register routes and render any template it ships, which is how you add pages such as `/features`, `/about` or a pricing explainer.
## What you need
| Piece | Purpose |
| - | - |
| `extra.entry-class` and `autoload.psr-4` in the manifest | So Composer can load your PHP |
| An entry class implementing `Plugin\Domain\PluginInterface` | Registers the directory to scan for routes |
| A request handler with a `#[Route]` attribute | Serves the page |
| A template under `templates/` | The page itself |
| `composer require` | Registers the autoloader |
## 1. Declare the class
```json static/composer.json theme={null}
{
"name": "acme/aurora",
"type": "aikeedo-theme",
"require": { "heyaikeedo/composer": "^1.0.0" },
"extra": {
"entry-class": "Acme\\Aurora\\Theme",
"title": "Aurora"
},
"autoload": {
"psr-4": { "Acme\\Aurora\\": "src/" }
}
}
```
## 2. Write the entry class
```php static/src/Theme.php theme={null}
mapper->addPath(__DIR__);
}
}
```
The class is resolved from the container, so anything you type-hint is injected. `boot()` runs only while your theme is the published one.
## 3. Add a request handler
```php static/src/FeaturesView.php theme={null}
isLandingPageEnabled) {
return new RedirectResponse('/app');
}
return new ViewResponse('@theme/templates/features.twig');
}
}
```
| Piece | Why |
| - | - |
| `AbstractRequestHandler` | Gives the standard public middleware: error handling, body parsing, user resolution and locale |
| `#[Middleware(ViewMiddleware::class)]` | Renders `ViewResponse` and adds the template globals |
| `[locale:locale]?` | Makes the page work under a language prefix such as `/de-DE/features` |
| `@theme/...` | Loads the template from the active theme |
## 4. Add the template
```twig static/templates/features.twig theme={null}
{% extends "@theme/layouts/theme.twig" %}
{% block title %}{{ dp__('theme', 'title', 'Features') }}{% endblock %}
{% block template %}
{% include "@theme/sections/header.twig" %}
{% include "@theme/sections/footer.twig" %}
{% endblock %}
```
## 5. Install and clear the cache
```bash theme={null}
composer require acme/aurora
```
Routes are discovered by scanning, and the result is cached when `CACHE=true`. During development set `CACHE=false`; in production clear the cache from **Status → Clear cache** after publishing a theme that adds routes.
## Passing data to the template
```php theme={null}
return new ViewResponse('@theme/templates/features.twig', [
'highlights' => $this->highlights(),
]);
```
To show plans on a page of your own, dispatch the same command the landing page uses:
```php theme={null}
use Billing\Application\Commands\ListPlansCommand;
use Presentation\Resources\Api\PlanResource;
use Shared\Infrastructure\CommandBus\Dispatcher;
$cmd = new ListPlansCommand();
$cmd->setStatus(1);
$cmd->setOrderBy('price', 'ASC');
$plans = [];
foreach ($this->dispatcher->dispatch($cmd) as $plan) {
$plans[] = new PlanResource($plan);
}
return new ViewResponse('@theme/templates/pricing.twig', ['plans' => $plans]);
```
Check the command's setters in the source before copying this: they've changed between versions, and the landing handler is the reference for what the current one expects.
## Rules
* **Keep the theme's pages public.** A page that needs a signed-in user belongs in a plugin under `/app`, not a theme.
* **Handle the landing-page switch.** If the public site is disabled, redirect rather than rendering.
* **Don't do heavy work.** Marketing pages should hit the database rarely, and never call a third-party API on render.
* **Namespace your classes** under your own vendor, so two themes can be installed at once without colliding.
## Troubleshooting
The theme isn't published, the cache is stale, or `addPath()` doesn't cover the handler's directory.
Run `composer require acme/aurora`, and check that the manifest's namespace, the PSR-4 map and the class all agree.
`#[Middleware(ViewMiddleware::class)]` is missing, so the `ViewResponse` was never rendered.
## Related
* [Theme structure](/development/themes/structure)
* [Templates and layouts](/development/themes/templates-and-layouts)
* [Plugin routes and handlers](/development/plugins/routes-and-request-handlers)
# Customizing an existing theme
Source: https://docs.aikeedo.com/development/themes/customizing-existing-themes
Adapt the default or a marketplace Aikeedo theme without losing your changes when the platform updates.
Starting from a theme that already works is often faster than building one. The rule that matters: **always work on a copy under your own vendor name**, because an update replaces the bundled theme's files.
Editing `extra/extensions/heyaikeedo/default` directly means the next Aikeedo update overwrites your work. Copy it first.
## Option 1: Copy, no build step
Good for text, colors and layout tweaks in the existing templates.
```bash theme={null}
cp -r extra/extensions/heyaikeedo/default extra/extensions/acme/aurora
```
In `extra/extensions/acme/aurora/composer.json`, change `name` to `acme/aurora` and `extra.title` to your theme's name. The directory path must match the name.
```bash theme={null}
cp -r public/e/heyaikeedo/default public/e/acme/aurora
```
The originals were published when the bundled theme was installed. Copying them gives your theme the same CSS, JavaScript and images.
In the admin panel, open **Themes** and publish your copy. Preview it first at `/preview?theme=acme/aurora`.
Change the Twig files in `extra/extensions/acme/aurora/`. With `CACHE=false` the changes appear immediately; otherwise clear the cache.
Your theme survives Aikeedo updates, because nothing ships a package under your vendor name.
**Limits of this approach:** the CSS is already compiled, so you can't add Tailwind classes that weren't used before, and there's no hot reloading. Restyling is easier from source.
## Option 2: Rebuild from source
Good for real design work: new sections, new classes, changed JavaScript.
```bash theme={null}
git clone https://github.com/heyaikeedo/themes-default.git aurora
cd aurora
npm install
```
Then follow the [quickstart](/development/themes/quickstart) from the renaming step: change `static/composer.json`, set `BUILD_DIR` to `extra/extensions/acme/aurora`, and run `npm run dev`.
You get the default theme's markup as a starting point, plus the full build pipeline.
## Option 3: Start from the starter kit
If you're replacing the design anyway, the [starter kit](https://github.com/heyaikeedo/themes-starter) is a smaller base: a layout, a header, the script-tag snippets and the custom elements, with no marketing sections to delete.
## Marketplace themes
A purchased theme is installed like any other package. To customize it, copy it under your own vendor name first, exactly as in option 1. Keep a copy of the original archive: when the author ships an update, you'll want to compare their changes against yours.
## Keeping changes maintainable
* **Track it in git**, even a private repository. A theme is source code.
* **Keep a change log** of what you altered relative to the original, so a future update is a diff rather than an archaeology exercise.
* **Prefer adding files over editing them.** A new section you include is easier to carry forward than a rewritten one.
* **Don't fork what you can configure.** Logos, colors, policies and social links are admin settings; read them instead of hardcoding.
## What you can't change in a theme
Auth pages, the app, the admin panel and error pages are core templates. Use `resources/views/overrides/` for those, and see [Extending Aikeedo](/development/extending-aikeedo).
## Related
* [Theme quickstart](/development/themes/quickstart)
* [Landing page settings](/website-basics/landing-page)
* [Packaging and publishing](/development/themes/packaging-and-publishing)
# Dark mode and colors
Source: https://docs.aikeedo.com/development/themes/dark-mode-and-colors
Implement light and dark modes in an Aikeedo theme, and respect the color scheme administrators configure.
Aikeedo lets administrators decide which color modes a site offers and which one is the default. A theme reads that setting, applies the right mode before the page paints, and styles both.
## The setting
`option.color_scheme` holds:
| Key | Meaning |
| - | - |
| `modes` | Which modes are available, for example `['light', 'dark']`, `['light']` or `['dark']` |
| `default` | `system`, `light` or `dark` |
| `accent`, `accent_content` | Accent colors, exposed as `{ hex, r, g, b, rgb }` |
## Apply the mode before paint
Resolve the mode in the document head, so there's no flash of the wrong theme:
```twig snippets/js.twig theme={null}
```
The result is ``, which is what your CSS keys off.
## Define the palette
Declare colors as space-separated RGB channels, so Tailwind can apply opacity to them:
```twig snippets/css.twig theme={null}
```
```javascript tailwind.config.js theme={null}
export default {
content: ['!./node_modules/**/*', './**/*.twig', './src/**/*.{js,css}'],
darkMode: ['class', '[data-mode="dark"]'],
theme: {
extend: {
colors: {
main: 'rgb(var(--color-main) / )',
content: 'rgb(var(--color-content) / )',
'content-dimmed': 'rgb(var(--color-content-dimmed) / )',
line: 'rgb(var(--color-line) / )',
accent: 'rgb(var(--color-accent) / )',
},
},
},
};
```
With `darkMode` bound to the attribute, `dark:` variants work as usual:
```html theme={null}
…
```
Because every color is a variable, one block of overrides under `[data-mode="dark"]` themes the whole site. Avoid hardcoding hex values in templates.
## The switcher
Show a toggle only when more than one mode is available:
```twig theme={null}
{% if option.color_scheme.modes is not defined or option.color_scheme.modes|length > 1 %}
{% endif %}
```
The starter's `mode-switcher` element flips `localStorage.mode` and updates `document.documentElement.dataset.mode`, so the choice survives navigation.
## Using the configured accent
The accent color is exposed ready to drop into a variable:
```twig theme={null}
--color-accent: {{ option.color_scheme.accent.rgb|default('99 102 241') }};
--color-accent-content: {{ option.color_scheme.accent_content.rgb|default('255 255 255') }};
```
| Property | Example |
| - | - |
| `hex` | `#6366f1` |
| `r`, `g`, `b` | `99`, `102`, `241` |
| `rgb` | `99 102 241`, formatted for CSS variables |
The bundled themes ship fixed palettes and ignore the accent setting. Reading it makes your theme adjustable from the admin panel without a rebuild, which is usually what customers expect.
## Checklist
The mode is applied in the head, before the body renders.
Only the configured modes are reachable, and the switcher hides when there's one.
Both modes are checked for contrast, including borders and dimmed text.
Images and logos have dark variants where they need them, for example `option.brand.logo_dark`.
The choice persists across page loads.
## Related
* [Templates and layouts](/development/themes/templates-and-layouts)
* [JavaScript components](/development/themes/javascript-components)
* [Branding settings](/website-basics/branding)
# JavaScript and components
Source: https://docs.aikeedo.com/development/themes/javascript-components
Add interactivity to an Aikeedo theme with Alpine.js and custom elements for money, credits, avatars and the dark-mode switcher.
Themes ship their own JavaScript bundle. The starter uses Alpine.js for page behavior and a handful of custom elements for formatting that's easier in the browser than in Twig.
## The entry point
```javascript src/js/index.js theme={null}
'use strict';
import Alpine from 'alpinejs';
import { ModeSwitcher } from './components/mode.js';
import { MoneyElement } from './components/money.js';
import { CreditElement } from './components/credit.js';
import { AvatarElement } from './components/avatar.js';
customElements.define('mode-switcher', ModeSwitcher);
customElements.define('x-money', MoneyElement);
customElements.define('x-credit', CreditElement);
customElements.define('x-avatar', AvatarElement);
Alpine.start();
```
The layout loads it as a module:
```twig theme={null}
```
This is your theme's bundle, separate from the application's. Nothing the app registers, such as its API client or modal controller, is available on the public site.
## Alpine
Add `x-data` to the body in the layout so small inline components work anywhere on the page:
```twig theme={null}
```
Then use Alpine directly in templates:
```twig theme={null}
```
For anything larger, register a named component before Alpine starts:
```javascript theme={null}
document.addEventListener('alpine:init', () => {
Alpine.data('pricing', () => ({
cycle: 'monthly',
toggle(value) { this.cycle = value; },
}));
});
```
```twig theme={null}
```
## The custom elements
### ``
Formats an amount in minor units using the browser's locale support.
| Attribute | Purpose |
| - | - |
| `data-value` | Amount in minor units, for example `2900` |
| `currency` | Currency code, such as `USD` |
| `minor-units` | Fraction digits for the currency |
| `fraction` | `auto`, `true` or `false` |
| `currency-display` | `symbol` by default |
| `content-type` | `rich` wraps parts in spans, `simple` renders plain text |
```twig theme={null}
```
### ``
Formats a credit balance, and handles the unlimited case.
| Attribute | Purpose |
| - | - |
| `data-value` | The credit amount; an empty value means unlimited |
| `format` | A template containing `:count` |
| `format-unlimited` | Text to show when there's no limit |
```twig theme={null}
```
### ``
Renders an image, an icon or initials.
| Attribute | Purpose |
| - | - |
| `src` | Image URL |
| `title` | Name, used to derive initials |
| `icon` | A Tabler icon name, or raw SVG markup |
| `length` | How many initials to show |
```twig theme={null}
```
### ``
Wraps a button and toggles the color mode, persisting the choice.
```twig theme={null}
```
## Globals the theme sets
```twig snippets/js.twig theme={null}
```
`window.currency` gives the components a fallback currency, and your own scripts a formatting reference.
## Translating strings in JavaScript
```javascript src/js/translate.js theme={null}
export function __(str) {
return window.locale?.messages[str] || str;
}
```
The lookup reads a catalog exposed as `window.locale`, and falls back to the original string, so untranslated builds still render. Prefer translating in Twig where you can; use this only for strings created in the browser.
## Writing your own element
```javascript src/js/components/countdown.js theme={null}
export class CountdownElement extends HTMLElement {
static observedAttributes = ['data-until'];
connectedCallback() {
this.render();
this.timer = setInterval(() => this.render(), 1000);
}
disconnectedCallback() {
clearInterval(this.timer);
}
render() {
const until = new Date(this.dataset.until);
const seconds = Math.max(0, Math.round((until - Date.now()) / 1000));
this.textContent = `${seconds}s`;
}
}
```
Register it alongside the others in `index.js`, and always clean up timers and listeners in `disconnectedCallback()`.
## Keep it light
* The landing page is what search engines and first-time visitors measure. Ship as little JavaScript as you can.
* Alpine plus a few custom elements covers most marketing pages; reach for a framework only when you truly need one.
* Load third-party embeds lazily, and put tracking in the [script tag snippets](/development/themes/script-tags-and-analytics) rather than your bundle.
## Related
* [Build and assets](/development/themes/build-and-assets)
* [Dark mode and colors](/development/themes/dark-mode-and-colors)
* [Pricing and plans](/development/themes/pricing-and-plans)
# Theme localization
Source: https://docs.aikeedo.com/development/themes/localization
Translate an Aikeedo theme: use the theme gettext domain, build catalogs with the console commands, and ship them with your package.
Themes have their own translation domain, separate from the application's. Getting the domain right is the difference between a translated theme and one that silently falls back to English.
## Use the theme domain
```twig theme={null}
{{ d__('theme', 'Get started') }}
{{ dp__('theme', 'heading', 'Pricing') }}
{{ dn__('theme', '%d seat', '%d seats', count)|format(count) }}
```
| Helper | Use |
| - | - |
| `d__('theme', text)` | A plain string |
| `dp__('theme', context, text)` | A string with a context, such as `button` or `heading` |
| `dn__('theme', singular, plural, count)` | A plural |
| `dnp__('theme', context, singular, plural, count)` | A plural with a context |
A plain `__()` in a theme template looks up the **application's** catalog. Your translation won't be found, and the extractor won't add the string to your catalogs.
## Catalog layout
```text theme={null}
static/locale/
├── de-DE/LC_MESSAGES/theme.po
├── es-ES/LC_MESSAGES/theme.po
└── fr-FR/LC_MESSAGES/theme.po
```
* One directory per language, named with the `xx-XX` code.
* The file is always `theme.po`.
* The header sets `X-Domain: theme`.
```po static/locale/de-DE/LC_MESSAGES/theme.po theme={null}
msgid ""
msgstr ""
"Language: de-DE\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Domain: theme\n"
msgctxt "button"
msgid "Get started"
msgstr "Loslegen"
```
The application loads the active theme's catalog for the resolved language on every request.
## Extract strings
Run the console commands from the Aikeedo installation, pointing at your theme's repository:
```bash theme={null}
php bin/console app:locale:extract --path=../aurora
```
The command finds your package inside the repository, scans the templates, and writes catalogs next to the manifest, using the `theme` domain.
| Option | Purpose |
| - | - |
| `--path`, `-p` | The theme repository, or any directory to scan |
| `--target`, `-t` | An installed package name instead of a path |
| `--locale-dir` | Where to write catalogs, defaulting to `/locale` |
| `--domain` | Override the domain, which is normally read from the manifest |
| `--language`, `-l` | Restrict to specific languages |
| `--no-prune` | Keep entries no longer found in the source |
The starter kit used to ship its own extraction script. It was removed: use these console commands, which share the application's parser and won't mangle quoting.
## Translate
```bash theme={null}
php bin/console app:locale:translate --path=../aurora --all
```
This fills in missing entries with an AI model. Add `--force` to redo existing translations, and `--dry-run` to preview. Review the output before release: marketing copy is where machine translation is weakest.
## Working on the installed copy
Point `--path` at your **repository**, not at `extra/extensions/...`. The installed copy is overwritten by the next build, so edits there are lost.
## When a language shows English
The theme's language is resolved from the URL prefix, then the user's preference, then the `locale` cookie, then the browser's `Accept-Language`, then the default. Three things must line up:
The language is enabled in **Settings → Languages**.
Your theme has a `theme.po` for it.
The string uses the `theme` domain.
If the theme has no catalog for the resolved language, the `locale` global is `null`, which is why every template should use `locale.code|default('en-US')`.
## Translating JavaScript strings
Strings that only exist in the browser go through the theme's own small helper, which reads a catalog exposed as `window.locale`. Prefer translating in Twig and passing the result into your component, and keep the JavaScript path for strings built at runtime.
## RTL languages
Each locale carries a `dir`. Set it on the document and use logical CSS properties so layouts mirror correctly:
```twig theme={null}
```
## Checklist
Every visible string uses a `d`-prefixed helper with the `theme` domain.
Contexts are used for short strings such as buttons and headings.
Catalogs exist for the languages you support, with `X-Domain: theme`.
Extraction was re-run after the last copy change.
`static/locale/` is included in your release archive.
The layout handles `dir` and a `null` locale.
## Related
* [Twig reference](/development/themes/twig-reference)
* [Localization settings](/advanced/localization)
* [Plugin localization](/development/plugins/localization)
# Theme manifest
Source: https://docs.aikeedo.com/development/themes/manifest
Every composer.json field an Aikeedo theme uses, with validation rules and the fields that behave differently from plugins.
A theme's `composer.json` is both its Composer manifest and the metadata Aikeedo shows in the admin panel.
## Example
```json composer.json theme={null}
{
"name": "acme/aurora",
"description": "A bold landing page theme",
"version": "1.2.0",
"type": "aikeedo-theme",
"homepage": "https://acme.test/themes/aurora",
"time": "2026-09-15",
"license": "proprietary",
"authors": [
{ "name": "Acme", "email": "hello@acme.test" }
],
"support": {
"docs": "https://docs.acme.test/aurora",
"email": "support@acme.test"
},
"require": {
"heyaikeedo/composer": "^1.0.0"
},
"extra": {
"entry-class": "Acme\\Aurora\\Theme",
"title": "Aurora",
"tagline": "A bold landing page",
"description": "A bold landing page theme",
"logo": "https://acme.test/aurora/logo.svg",
"icon": "https://acme.test/aurora/icon.svg",
"public": ["assets", ".vite"]
},
"autoload": {
"psr-4": {
"Acme\\Aurora\\": "src/"
}
}
}
```
## Required
Must be `aikeedo-theme`.
The package name, `vendor/package`. It must match the installed directory, `extra/extensions/{vendor}/{package}`.
The installer package. Use `^1.2.0` if you use object entries in `extra.public`.
A manifest that breaks any of these is rejected, and the theme never appears in the admin panel.
## Differences from plugins
| Field | Plugin | Theme |
| - | - | - |
| `extra.entry-class` | Required | **Optional.** Only needed for custom pages. |
| `extra.status` | Aikeedo writes `active` or `inactive` | **Ignored.** The active theme is the one published in the admin panel. |
| `extra.default_url` | Links from the plugins list | Not used |
## Metadata
Semantic version. Installing from an archive runs `composer require :`, so keep it accurate and raise it every release.
Shown in the admin panel. The top-level field is used, not `extra.description`.
Display name. Falls back to the package name.
A short one-line summary.
URL of a logo image for listings.
URL of an icon image.
Your product page.
Release date, such as `2026-09-15`.
Entries with `name`, `email`, `homepage` and `role`.
Any of `chat`, `docs`, `email`, `forum`, `irc`, `issues`, `rss`, `source`, `wiki`.
## Assets
Directories and files copied to `public/e/{vendor}/{package}/` when the theme is installed. A built theme normally publishes `["assets", ".vite"]`: the compiled files, and the manifest that maps source paths to their hashed names.
Without this, your CSS and JavaScript aren't reachable from the browser. See [Build and assets](/development/themes/build-and-assets).
## PHP
A class implementing `Plugin\Domain\PluginInterface`. Only needed when the theme registers routes or services. Escape the backslashes: `"Acme\\Aurora\\Theme"`.
Maps your namespace to `src/`. Required whenever the theme ships PHP, and the package must be installed with Composer for it to take effect.
## Checklist
The installed directory matches `name`.
`type` is `aikeedo-theme`.
`require` includes `heyaikeedo/composer`.
`version` is set and raised for the release.
`extra.public` lists everything the browser needs.
If there's PHP, the entry class and the PSR-4 namespace agree.
## Related
* [Packaging and publishing](/development/themes/packaging-and-publishing)
* [Custom pages](/development/themes/custom-pages)
* [Plugin manifest](/development/plugins/manifest)
# The minimum theme
Source: https://docs.aikeedo.com/development/themes/minimum-theme
Build the smallest working Aikeedo theme: one manifest and one template, with no build step.
You don't need Vite, Tailwind or PHP to ship a theme. Aikeedo needs a valid package and a landing template. Start here when you want a hand-written page, or when you're learning how themes are discovered.
## Two files
```text theme={null}
extra/extensions/acme/minimal/
├── composer.json
└── templates/
└── index.twig
```
```json extra/extensions/acme/minimal/composer.json theme={null}
{
"name": "acme/minimal",
"description": "A minimal Aikeedo theme",
"version": "1.0.0",
"type": "aikeedo-theme",
"require": {
"heyaikeedo/composer": "^1.0.0"
},
"extra": {
"title": "Minimal"
}
}
```
```twig extra/extensions/acme/minimal/templates/index.twig theme={null}
{{ option.site.name ?? 'Aikeedo' }}
{{ option.site.name ?? 'Aikeedo' }}
{% if user is defined %}
{{ d__('theme', 'Open the app') }}
{% else %}
{{ d__('theme', 'Sign in') }}{{ d__('theme', 'Get started') }}
{% endif %}
```
That's a working theme. Publish it in **Admin → Themes**.
## Why it works
| Requirement | Satisfied by |
| - | - |
| Discoverable | A directory under `extra/extensions/` whose path matches the package `name` |
| Valid | `type: aikeedo-theme`, a `name`, and `heyaikeedo/composer` in `require` |
| Renderable | `templates/index.twig`, which the landing route renders through the `@theme` namespace |
An entry class is optional for themes, so no PHP and no `autoload` block are needed. `extra.status` is ignored: a theme is active when it's the one published in the admin panel.
Set `version` even here. Installing a theme from an archive runs Composer with that version, and a missing or stale one makes installs unpredictable.
## Adding pieces as you need them
| Add | When |
| - | - |
| `layouts/`, `sections/`, `snippets/` | The page grows past one file. Include them with `@theme/...` paths. |
| `assets/` plus `extra.public` | You have CSS, JavaScript or images to serve |
| `.vite/manifest.json` | You build with Vite and want hashed filenames resolved automatically |
| `locale/` | You want the theme translated |
| `src/` plus `extra.entry-class` | You want extra public pages |
## Rules that still apply
* **Guard optional variables.** Debug mode enables strict Twig variables, so `{% if user is defined %}` and `|default(...)` matter even in a small theme.
* **Use the theme translation domain.** `d__('theme', '...')` and `dp__('theme', 'context', '...')` resolve against your catalogs; a plain `__()` looks in the application's.
* **Honor admin settings.** Reading `option.site.name`, `option.brand.logo` and `option.policies.*` keeps the admin panel meaningful.
* **Include the script-tag snippets** if you want the analytics codes administrators configure to work. See [Script tags and analytics](/development/themes/script-tags-and-analytics).
## Related
* [Theme structure](/development/themes/structure)
* [Theme manifest](/development/themes/manifest)
* [Templates and layouts](/development/themes/templates-and-layouts)
# Aikeedo themes
Source: https://docs.aikeedo.com/development/themes/overview
What an Aikeedo theme controls, how the active theme is resolved and rendered, and when to use a theme instead of a plugin or a view override.
A theme is a Composer package that renders your public website. It owns the landing page, and it can add public pages of its own. Everything behind the login screen belongs to the application.
This page applies to Aikeedo 5.x. Class names, paths and signatures match the 5.0 source code.
## What a theme controls
| Page | Rendered by |
| - | - |
| Landing page, `/` | **Your theme**, from `templates/index.twig` |
| Extra public pages, such as `/features` | **Your theme**, if it registers routes through an entry class |
| Login, signup, password recovery | Core templates |
| Policy pages, such as `/policies/terms` | Core templates, with content from admin settings |
| The app, `/app`, and the admin panel | Core templates |
| 404 and error pages | Core templates |
To change a core template, use `resources/views/overrides/`, which isn't part of the theme system. See [Extending Aikeedo](/development/extending-aikeedo).
## How rendering works
```mermaid theme={null}
flowchart TD
A["GET /"] --> B{Landing page enabled?}
B -- no --> C[Redirect to /app]
B -- yes --> D[IndexRequestHandler]
D --> E[Load active plans]
E --> F["Render @theme/templates/index.twig"]
F --> G[ViewMiddleware adds globals: option, user, currency, locale…]
G --> H[HTML]
```
* The active theme is the package named by the `theme` option, which defaults to the bundled default theme. Publishing a theme in **Admin → Themes** sets it.
* `@theme` is a Twig namespace pointing at the active theme's directory, so every include in your templates starts with `@theme/`.
* The landing page receives `plans`, the active plans sorted by price. Every other global comes from the view middleware.
* `site.is_landing_page_enabled` turns the landing page off, which redirects `/` to the app.
## Preview before publishing
`/preview?theme=vendor/name` sets a short-lived preview cookie and sends you to the home page, so you can look at a theme without switching the live site.
## Anatomy
```text theme={null}
extra/extensions/acme/aurora/
├── composer.json # type: aikeedo-theme
├── templates/
│ └── index.twig # the landing page
├── layouts/
├── sections/
├── snippets/
├── assets/ # built CSS, JS and images
├── .vite/manifest.json # written by the build
├── src/ # optional PHP entry class and page handlers
└── locale/ # translation catalogs, theme domain
```
Only `composer.json` and `templates/index.twig` are required. Everything else is convention, plus whatever your build produces. See [The minimum theme](/development/themes/minimum-theme).
## Where themes come from
| Approach | Use when |
| - | - |
| [The starter kit](/development/themes/quickstart) | You're building a new theme. It ships the build pipeline, a layout, custom elements and catalogs. |
| [Copy the default theme](/development/themes/customizing-existing-themes) | You want the shipped design with changes. Always rename it, or an update overwrites your work. |
| From scratch | You want full control and no build step. Start from the minimum theme. |
Want a finished design instead of building one? The official Pulse theme is available on the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content):
## Theme development at a glance
Clone the starter kit and get hot reloading against a local installation.
What every directory and file is for.
Every variable available in a theme template.
Add public pages with a PHP entry class.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Packaging and publishing
Source: https://docs.aikeedo.com/development/themes/packaging-and-publishing
Build an installable Aikeedo theme archive, version it, and distribute it to customers.
A theme ships as a ZIP whose root holds `composer.json`. The starter kit's scripts produce that for you.
## Build commands
| Command | Result |
| - | - |
| `npm run build` | Production build into `BUILD_DIR` |
| `npm run pack` | Builds into a temporary directory, then zips it into `theme.zip` |
| `npm run release` | Runs `pack`, then wraps `theme.zip` and the `release/` folder into a versioned archive |
```bash theme={null}
npm run pack # theme.zip
npm run pack -- aurora.zip # a name of your choosing
npm run release # aurora-v1.2.0-a1b2c3d.zip
```
The release name comes from the package name, the latest git tag and the short commit hash, so tag your releases before running it.
## What's inside
`theme.zip` has no wrapper folder. Its root is the package:
```text theme={null}
theme.zip
├── composer.json
├── .vite/manifest.json
├── assets/
│ ├── css-a1b2c3.css
│ ├── js-d4e5f6.js
│ └── logo.svg
├── layouts/ templates/ sections/ snippets/
├── locale/
└── src/ # only if the theme ships PHP
```
The release archive adds the contents of `release/`, normally an installation README, alongside `theme.zip`.
## Before you package
`version` in `static/composer.json` is raised.
`extra.public` lists `assets` and `.vite`, so the built files reach the web root.
Translation catalogs are extracted and up to date.
The entry class namespace matches the PSR-4 map, if the theme ships PHP.
`THEME_ASSETS_SERVER` is unset in the installation you tested against, so you tested the built assets.
Nothing private is in `static/`: no `.env`, keys or customer data.
The theme is renamed if it started as a copy, so an update can't overwrite it.
## Installing a packaged theme
In the admin panel, go to **Themes → Install theme** and upload `theme.zip`. The archive is validated, stored, and installed with Composer.
Find the theme in the list and select **Publish**. Preview it first at `/preview?theme=vendor/name`.
Load the site with caching on, and check that CSS, JavaScript, images and translations all work.
Installing over an existing copy backs up the previous version first, and restores it if the install fails.
## Versioning
* Use semantic versioning, and raise `version` for every release: the installer runs `composer require :`, so a stale version installs the wrong code.
* Note which Aikeedo versions the theme supports in your README. A theme that reads a newer template object won't work on an older installation.
* Keep previous archives available for customers who haven't updated Aikeedo yet.
## Writing the README
Ship one in `release/`, and cover:
* What the theme looks like, and which Aikeedo versions it supports
* How to install and publish it
* Which admin settings it reads, such as brand logos, colors, policies and social links
* Which sections can be turned on and off, and how
* How to translate it
* A changelog
## Distribution
| Option | Notes |
| - | - |
| Direct download | Send the ZIP; customers upload it. Updates are manual. |
| Private Composer repository | Customers run `composer require`, which suits teams managing installs from the CLI. |
We're looking for community-built themes to list on aikeedo.com, linking to your own page. See what to send us.
## Updating a released theme
A customer updates by uploading the newer archive, which replaces the package and republishes its assets. Two things to keep in mind:
* **Customers may have edited your templates.** Say so in the README: changes made inside `extra/extensions/` are replaced on update.
* **Don't rename template files casually.** A customer who overrode `sections/hero.twig` loses the override when it disappears.
## Related
* [Build and assets](/development/themes/build-and-assets)
* [Theme manifest](/development/themes/manifest)
* [Customizing an existing theme](/development/themes/customizing-existing-themes)
# Pricing and plans
Source: https://docs.aikeedo.com/development/themes/pricing-and-plans
Render Aikeedo subscription plans on a theme's landing page, including billing cycles, credits, features and checkout links.
The landing page is the only theme template that receives `plans`. It holds the active plans, sorted by price ascending, each serialized with everything a pricing table needs.
## What a plan contains
| Field | Type | Notes |
| - | - | - |
| `id` | string | Use it to build the checkout link |
| `title` | string | Plan name |
| `description` | string or null | Short summary |
| `icon` | string or null | An icon name, or raw SVG markup |
| `price` | integer | Minor units, for example `2900` for 29.00 |
| `sale_price` | integer or null | Set when a coupon applies |
| `billing_cycle` | string | `one-time`, `monthly`, `yearly` or `lifetime` |
| `credit_count` | integer or null | `null` means unlimited |
| `member_cap` | integer or null | Workspace member limit, `null` for unlimited |
| `is_featured` | boolean | Highlight this plan |
| `superiority` | integer | Ordering weight for comparison tables |
| `feature_list` | array | Objects of `{ title, is_included }` |
| `config` | object | Capability configuration, see below |
| `coupon` | object or null | The applied coupon |
| `discount` | number or null | Discount amount when a coupon applies |
| `snapshot` | object | The plan snapshot subscriptions are based on |
A feature line entered as `-No API access` becomes `{ title: 'No API access', is_included: false }`, so you can render excluded features with a different marker.
## A pricing section
```twig sections/pricing.twig theme={null}
{{ dp__('theme', 'heading', 'Pricing') }}
{% for plan in plans|default([]) %}
{% include "@theme/snippets/plan.twig" with { plan } %}
{% endfor %}
{{ dp__('theme', 'button', 'Get started') }}
```
## Formatting money and credits
Two custom elements do the formatting for you:
```html theme={null}
```
| Element | Attributes |
| - | - |
| `` | `data-value` in minor units, `currency`, `minor-units`, `fraction` (`auto`, `true`, `false`), `currency-display` |
| `` | `data-value`, `format` with a `:count` placeholder, `fraction`. A `null` value renders as unlimited. |
The `currency` global gives you what they need:
```twig theme={null}
{{ currency.code }} {# USD #}
{{ currency.symbol }} {# $, or null when it equals the code #}
{{ currency.fraction_digits }} {# 2 #}
```
## Grouping by billing cycle
Most pricing pages separate recurring plans from one-time packs:
```twig theme={null}
{% set monthly = plans|filter(p => p.billing_cycle == 'monthly' and p.price > 0) %}
{% set yearly = plans|filter(p => p.billing_cycle == 'yearly' and p.price > 0) %}
{% set packs = plans|filter(p => p.billing_cycle == 'one-time') %}
{% set free = plans|filter(p => p.price == 0) %}
```
For a monthly and yearly toggle, render both groups and switch between them with Alpine:
```twig theme={null}
{% for plan in monthly %}{% include "@theme/snippets/plan.twig" with { plan } %}{% endfor %}
{% for plan in yearly %}{% include "@theme/snippets/plan.twig" with { plan } %}{% endfor %}
```
To show a yearly plan as a monthly figure, divide by twelve and say what's actually billed:
```twig theme={null}
{{ dp__('theme', 'pricing', 'billed annually') }}
```
## Feature rows from plan capabilities
`plan.config` mirrors what the plan grants, so a comparison table can be generated rather than hand-written:
```twig theme={null}
{% if option.features.chat.is_enabled|default(false) %}
{{ d__('theme', 'AI chat') }}
{% endif %}
```
Check the global feature flag as well as the plan, so a capability the administrator turned off platform-wide doesn't appear in your table.
## Checkout links
```twig theme={null}
{{ dp__('theme', 'button', 'Get started') }}
```
Signed-out visitors are sent to sign in first, and return to checkout afterwards, so you don't need to branch on `user`.
## Credit pricing pages
Because the model registry and credit rates are exposed to templates, you can build a calculator that shows what credits buy:
```twig theme={null}
{% for service in config.model.registry.directory|default([]) %}
{% for rate in service.rates|default([]) %}
{{ service.name }}
{{ option.credit_rate[rate.key]|default('—') }}
{% endfor %}
{% endfor %}
```
## Related
* [Template objects](/development/themes/template-objects)
* [JavaScript components](/development/themes/javascript-components)
* [Billing overview](/billing/overview)
# Build your first theme
Source: https://docs.aikeedo.com/development/themes/quickstart
Clone the Aikeedo theme starter kit, connect it to a local installation with hot reloading, and publish your theme.
The [theme starter kit](https://github.com/heyaikeedo/themes-starter) gives you a working theme with Vite, Tailwind CSS, Alpine.js, translation catalogs and an optional PHP entry class. This guide takes it from clone to published.
## Prerequisites
* A local Aikeedo installation with `DEBUG=true` and `CACHE=false`. See [Local development](/development/local-development).
* Node.js 18 or newer, and Composer.
* Administrator access to the admin panel.
## Step 1: Clone the starter
```bash theme={null}
git clone https://github.com/heyaikeedo/themes-starter.git aurora
cd aurora
npm install
```
## Step 2: Rename the package
Edit `static/composer.json`. Three things must agree: the package name, the entry class namespace and the autoload map.
```json static/composer.json theme={null}
{
"name": "acme/aurora",
"description": "Aurora theme for Aikeedo",
"version": "1.0.0",
"type": "aikeedo-theme",
"require": {
"heyaikeedo/composer": "^1.0.0"
},
"extra": {
"entry-class": "Acme\\Aurora\\Theme",
"title": "Aurora",
"description": "Aurora theme for Aikeedo",
"status": "active",
"public": ["assets", ".vite"]
},
"autoload": {
"psr-4": {
"Acme\\Aurora\\": "src/"
}
}
}
```
Then update the namespace in `static/src/Theme.php` and `static/src/CustomView.php` to match.
The installed directory path must equal the package name. `acme/aurora` installs to `extra/extensions/acme/aurora`, and nothing loads if the two disagree.
## Step 3: Point the build at your installation
The starter reads `BUILD_DIR` and `AIKEEDO_SERVER` from its environment. Copy the committed defaults into a local file and set your own paths:
```bash theme={null}
cp .env .env.local
```
```ini .env.local theme={null}
# Where the built theme is written: the installed package directory
BUILD_DIR=/path/to/aikeedo/extra/extensions/acme/aurora
# Where the dev server proxies everything that isn't a theme asset
AIKEEDO_SERVER=http://localhost:8000
```
`BUILD_DIR` is emptied on every production build. Point it at the theme's own directory, never at the installation root or a directory holding anything else.
## Step 4: Tell Aikeedo to load assets from Vite
In the Aikeedo installation's `.env`:
```ini .env theme={null}
THEME_ASSETS_SERVER=http://localhost:5174
DEBUG=true
CACHE=false
```
## Step 5: Start the dev server
```bash theme={null}
npm run dev
```
Vite serves on port 5174 and proxies everything that isn't a theme asset to `AIKEEDO_SERVER`, so you can browse the whole site through it. It also copies `static/` into `BUILD_DIR` as you edit.
Open `http://localhost:5174`.
## Step 6: Install the theme
The starter ships a PHP entry class, which Composer has to autoload. From the installation root:
```bash theme={null}
composer require acme/aurora
```
A theme without PHP doesn't strictly need this: Aikeedo discovers any valid package directory under `extra/extensions/`. Installing with Composer is still the tidier path, and it's required as soon as you add an entry class.
## Step 7: Publish it
Visit `/preview?theme=acme/aurora` to see the theme without switching the live site.
In the admin panel, open **Themes**, find **Aurora**, and select **Publish**.
Load the home page. You should see the starter's landing page.
Editing `static/templates/index.twig` reloads the page, and editing `src/css/index.css` updates styles without a reload.
## Step 8: Make it yours
| To change | Edit |
| - | - |
| The page itself | `static/templates/index.twig` |
| The HTML shell, meta tags, scripts | `static/layouts/theme.twig` |
| Reusable blocks | `static/sections/*.twig` |
| Colors and fonts | `static/snippets/css.twig` and `tailwind.config.js` |
| Interactivity | `src/js/index.js` |
| Extra pages | `static/src/`, see [Custom pages](/development/themes/custom-pages) |
Add a pricing section next: the landing template receives `plans`, and [Pricing and plans](/development/themes/pricing-and-plans) shows how to render them.
## Step 9: Package a release
```bash theme={null}
npm run build # production build into BUILD_DIR
npm run pack # theme.zip, installable through the admin panel
npm run release # versioned archive including the release/ folder
```
See [Packaging and publishing](/development/themes/packaging-and-publishing).
## Troubleshooting
Check that the directory path matches the package name, that `type` is `aikeedo-theme`, and that `require` includes `heyaikeedo/composer`.
`THEME_ASSETS_SERVER` must match the Vite port, and `npm run dev` must be running. Without the dev server, build once so the files exist in `BUILD_DIR`.
Run `composer require acme/aurora`, and check that the namespace in `composer.json`, `Theme.php` and the autoload map all agree.
Routes are cached when `CACHE=true`. Set `CACHE=false`, or clear the cache from **Status → Clear cache**.
Debug mode enables strict variables. Guard optional values, for example `{% if user is defined %}`.
## Related
* [Theme structure](/development/themes/structure)
* [Build and assets](/development/themes/build-and-assets)
* [Template objects](/development/themes/template-objects)
* [Default theme source](https://github.com/heyaikeedo/themes-default)
# Script tags and analytics
Source: https://docs.aikeedo.com/development/themes/script-tags-and-analytics
Render the custom scripts, Google Tag Manager, Google Analytics and Intercom tags that Aikeedo administrators configure.
Administrators configure tracking and support widgets in the admin panel, but nothing injects them into your theme automatically. Your templates render them, which means a theme that skips this silently breaks features the site owner configured and paid for.
If your theme doesn't include these snippets, the analytics and support codes set in the admin panel never load on the public site. Include them, even in a minimal theme.
## Three insertion points
The convention is one snippet per position, included by the layout:
```twig layouts/theme.twig theme={null}
…
{%- include "@theme/snippets/script-tags/head.twig" -%}
{%- include "@theme/snippets/script-tags/body.twig" -%}
{% block template %}{% endblock %}
{%- include "@theme/snippets/script-tags/end.twig" -%}
```
| Snippet | Position | Typical contents |
| - | - | - |
| `head.twig` | End of `` | Custom head code, Tag Manager, Analytics, Intercom |
| `body.twig` | Right after `` | Custom body code, the Tag Manager `noscript` frame |
| `end.twig` | Before `` | Custom end-of-body code |
## Custom code blocks
Administrators can paste arbitrary markup, which may itself contain Twig-style placeholders. Compile it with the `template()` function before including it:
```twig snippets/script-tags/head.twig theme={null}
{% if option.script_tags.custom.head is defined and option.script_tags.custom.head %}
{{ include(template(option.script_tags.custom.head))|raw }}
{% endif %}
```
`template()` compiles a string into a template with strict variables disabled, so a typo in the pasted snippet doesn't take the page down.
## Google Tag Manager
```twig theme={null}
{% if option.script_tags.gtm is defined and option.script_tags.gtm.is_enabled and option.script_tags.gtm.container_id %}
{% endif %}
```
The matching `noscript` iframe belongs in `body.twig`:
```twig theme={null}
{% if option.script_tags.gtm is defined and option.script_tags.gtm.is_enabled and option.script_tags.gtm.container_id %}
{% endif %}
```
## Google Analytics
```twig theme={null}
{% if option.script_tags.ga.is_enabled|default(false) and option.script_tags.ga.measurement_id|default(false) %}
{% endif %}
```
## Intercom
Intercom needs visitor details when someone is signed in, and a signed hash when identity verification is on:
```twig theme={null}
{% if option.script_tags.intercom.is_enabled|default(false) and option.script_tags.intercom.app_id|default(false) %}
{% endif %}
```
`hash_hmac()` is available in templates precisely for this. It runs server-side, so the secret never reaches the browser.
Never print `option.script_tags.intercom.secret_key`, or any other secret, into the page. Only the computed hash belongs in the output.
## Guard everything
Every block above is wrapped in a check, for two reasons: debug mode raises errors on undefined variables, and an enabled integration with a missing ID emits a broken tag. Check both the toggle and the identifier.
## Copying the snippets
The fastest way to get this right is to copy the three snippets from the starter kit into your theme unchanged, then include them from your layout. They're plain Twig with no dependency on the rest of the starter.
## Related
* [Templates and layouts](/development/themes/templates-and-layouts)
* [Twig reference](/development/themes/twig-reference)
* [Custom scripts settings](/integrations/custom-scripts)
# Theme structure
Source: https://docs.aikeedo.com/development/themes/structure
How an Aikeedo theme project is laid out, which files the application requires, and what the build produces.
The starter kit separates **source** from **package**. You edit both, but only the package is installed.
```text theme={null}
aurora/
├── package.json # build scripts and dependencies
├── vite.config.mjs # dev server and build configuration
├── tailwind.config.js
├── postcss.config.js
├── .env # committed defaults: AIKEEDO_SERVER, BUILD_DIR
├── scripts/
│ ├── pack.mjs # builds and zips the installable theme
│ └── release.mjs # wraps theme.zip into a versioned archive
├── release/
│ └── README.md # shipped alongside theme.zip in the release archive
├── src/ # compiled sources: not shipped as-is
│ ├── css/
│ │ ├── index.css # CSS entry
│ │ ├── base.css
│ │ ├── button.css
│ │ ├── typography.css
│ │ └── avatar.css
│ └── js/
│ ├── index.js # JS entry
│ ├── translate.js
│ └── components/ # custom elements
└── static/ # the package: copied as-is into the build
├── composer.json
├── layouts/theme.twig
├── templates/
│ ├── index.twig
│ └── custom.twig
├── sections/header.twig
├── snippets/
│ ├── css.twig
│ ├── js.twig
│ └── script-tags/{head,body,end}.twig
├── assets/ # static images, logos
├── src/ # optional PHP
│ ├── Theme.php
│ └── CustomView.php
└── locale/{code}/LC_MESSAGES/theme.po
```
## What ends up installed
Building copies everything in `static/` to `BUILD_DIR`, adds the compiled CSS and JS under `assets/`, and writes `.vite/manifest.json`. The installed theme looks like this:
```text theme={null}
extra/extensions/acme/aurora/
├── composer.json
├── layouts/ templates/ sections/ snippets/ locale/ src/
├── assets/
│ ├── css-a1b2c3.css
│ ├── js-d4e5f6.js
│ └── logo.svg
└── .vite/manifest.json
```
Files listed in `extra.public` are then copied to `public/e/acme/aurora/`, which is what the browser actually loads.
## What each part does
| Path | Purpose |
| - | - |
| `static/composer.json` | The theme manifest. See [Theme manifest](/development/themes/manifest). |
| `static/layouts/theme.twig` | The HTML shell: head, meta, assets, `{% block template %}` |
| `static/templates/index.twig` | **Required.** The landing page. |
| `static/templates/*.twig` | Pages your entry class routes to |
| `static/sections/*.twig` | Large reusable blocks, such as a hero or pricing table |
| `static/snippets/*.twig` | Small partials: CSS variables, inline scripts, analytics tags |
| `static/assets/` | Images and other static files served as-is |
| `static/src/` | Optional PHP: the entry class and page handlers |
| `static/locale/` | Translation catalogs in the `theme` domain |
| `src/css`, `src/js` | Sources Vite compiles into `assets/` |
| `scripts/` | Packaging helpers |
Only `composer.json` and `templates/index.twig` are required by the application. Layouts, sections and snippets are conventions that make a theme easier to maintain.
## Naming conventions
* **Templates** are pages: one file per route.
* **Sections** are page-sized blocks, included from a template: `{% include "@theme/sections/hero.twig" %}`.
* **Snippets** are fragments used in several places, often with variables: `{% include "@theme/snippets/plan.twig" with { plan } %}`.
* **Layouts** are extended, not included: `{% extends "@theme/layouts/theme.twig" %}`.
Every path starts with `@theme/`, which points at the active theme's installed directory.
## PHP inside a theme
A theme that only renders the landing page needs no PHP. Add `static/src/` when you want extra pages:
| File | Role |
| - | - |
| `Theme.php` | The entry class named in `extra.entry-class`. It implements `Plugin\Domain\PluginInterface` and registers routes in `boot()`. |
| `CustomView.php` | A request handler with a `#[Route]` attribute, rendering one of your templates |
Anything under `static/src/` must be covered by the PSR-4 map in `composer.json`, and the package must be installed with Composer so the classes autoload. See [Custom pages](/development/themes/custom-pages).
## Translations
Catalogs live at `static/locale/{code}/LC_MESSAGES/theme.po`, in the `theme` domain. The starter ships catalogs for the locales Aikeedo supports. See [Theme localization](/development/themes/localization).
## Related
* [Build and assets](/development/themes/build-and-assets)
* [Templates and layouts](/development/themes/templates-and-layouts)
* [Theme manifest](/development/themes/manifest)
# Template objects reference
Source: https://docs.aikeedo.com/development/themes/template-objects
Every variable and object available in an Aikeedo theme template, with its fields, types and availability.
This is the complete set of data a theme template can read. Availability matters as much as shape: several of these exist only on certain pages or for signed-in visitors, and debug mode raises an error when you read something undefined.
## Availability at a glance
| Variable | Available |
| - | - |
| `option`, `config`, `version`, `license`, `theme`, `environment`, `nav`, `currency`, `locales`, `env` | Always |
| `locale` | When the resolved language has a theme catalog; otherwise `null` |
| `theme_locale` | Always, but deprecated. Use `locale.code`. |
| `user`, `workspace` | Only when a visitor is signed in |
| `plans` | Only on the landing page |
```twig theme={null}
{% if user is defined %}…{% endif %}
{{ plans|default([]) }}
{{ locale.code|default('en-US') }}
```
## Globals
Every platform setting, as a nested object: `option.site.name`, `option.brand.logo`, `option.policies.tos`, `option.features.chat.is_enabled`, and so on. A setting that was never saved is undefined, so guard it.
Two values are computed rather than stored:
* `option.site.url`, built from the configured domain and whether the site is served over HTTPS.
* `option.color_scheme.accent` and `.accent_content`, expanded into `{ hex, r, g, b, rgb }`.
Application configuration. Most themes only need `config.model.registry`, which lists AI services and models for pricing or capability tables.
The application version, also used as the cache-busting query on theme assets.
The installation's license text, or `null`.
The active theme's package name, such as `acme/aurora`.
`prod`, `dev`, `demo` or `install`.
The navigation registry. Themes rarely need it; it's what the app and admin sidebars are built from.
The raw environment. Read one key when you must, such as `env.THEME_ASSETS_SERVER`.
This contains database credentials and secret keys. Never print or serialize the whole object.
## Currency
The platform billing currency.
ISO code, such as `USD`.Localized name, such as `US Dollar`.The symbol, or `null` when it's the same as the code.Decimal places, such as `2`.
## Locales
Every configured language.
Language code, such as `de-DE`.English name, such as `German`.Whether an administrator enabled it.`ltr` or `rtl`.An alias of `code`, kept for older themes.
The language resolved for this request, in the same shape as a `locales` entry. It's `null` when the theme has no catalog for the resolved language, so always use `locale.code|default('en-US')`.
Deprecated. The resolved language code as a plain string.
```twig theme={null}
{% for item in locales %}
{% if item.enabled %}
{{ item.label }}
{% endif %}
{% endfor %}
```
## User
The signed-in visitor. Undefined for guests.
UUID.`admin` or `user`.Email address.First name.Last name.Phone number.Preferred language code.False for accounts created through a social login.How many workspaces the user may own; `null` means unlimited.Gravatar URL derived from the email address.Unix timestamp.Unix timestamp, or `null`.Last known IP address.See [Country](#country).City.Verification status.The user's API key, or `null`.See [Affiliate](#affiliate).The current workspace. Note that `owner` and `users` are **not** included on this nested copy.`memories`, `history`, `disabled_tools`, `instructions`, `preferred_name` and `marketing`.Workspaces the user belongs to but doesn't own. Present on the global `user` only.Workspaces the user owns. Present on the global `user` only.
## Workspace
The visitor's current workspace. Undefined for guests.
UUID.Workspace name.The workspace's own key, when it brings one.As above.See [Address](#address).Whether a trial is still available.Whether the free plan is still available.Unix timestamp.Unix timestamp, or `null`.Pending invitations. See [Workspace invitation](#workspace-invitation).Add-on credits; `null` means unlimited.Add-on plus subscription credits.See [Subscription](#subscription).`null` unless the installation bills by session; otherwise `{ resets_at, used_percent }`.Present only in session billing mode.The owning user. Present on the global `workspace` only.Members. Present on the global `workspace` only.
## Subscription
Available as `workspace.subscription`, or `null` when the workspace has none.
UUID.Trial length in days.Credits used in the current period.Percentage **with a `%` suffix**, such as `"45.00%"`.Credits remaining; `null` means unlimited.Percentage with a `%` suffix.Unix timestamp.Unix timestamp, or `null`.When the user cancelled.When access ends.Next usage reset; `null` once cancelled.Next payment date, following the plan's billing cycle.The plan snapshot. See [Plan snapshot](#plan-snapshot).The order's currency, or `null` when there's no order.Gateway key.The provider's reference.
## Plan
Active plans, sorted by price ascending. **Available on the landing page only.**
UUID, used in the checkout link.Plan name.Short description.An icon name, or raw SVG markup.Minor units, such as `2900` for 29.00.Discounted price when a coupon applies.`one-time`, `monthly`, `yearly` or `lifetime`.`null` means unlimited.Member limit; `null` means unlimited.Highlight this plan.Ordering weight for comparison tables.Unix timestamp.Unix timestamp, or `null`.Objects of `{ title, is_included }`. A line entered with a leading `-` becomes `is_included: false`.Capabilities. See [Plan config](#plan-config).The applied coupon.Discount amount when a coupon applies.The snapshot subscriptions are based on.
## Plan snapshot
A frozen copy of a plan, available as `subscription.plan`. It carries `id`, `title`, `description`, `icon`, `price`, `billing_cycle`, `credit_count`, `member_cap`, `created_at`, `updated_at`, `config` and `feature_list`, with the same meanings as on a plan. It has no `sale_price`, `coupon`, `discount` or `snapshot`.
## Plan config
What a plan grants. Available as `plan.config` and `subscription.plan.config`.
Chat capability settings.Writer settings.Image generation settings.Video settings.Transcription settings.Voice-over settings.Voice isolation settings.Classification settings.Allowed model keys; `null` means all.Tool switches, keyed by capability.Allowed assistants; `null` means all.Allowed templates; `null` means all.Per-plan settings added by plugins, keyed by extension.
## Affiliate
Available as `user.affiliate`. Every user has one.
UUID.Referral code.Tracked clicks.Referred signups.Available balance, converted into the billing currency.Pending balance, converted into the billing currency.Withdrawn total, converted into the billing currency.The currency those amounts are in.Payout email.Bank details.Chosen payout method.
## Address
Available as `workspace.address`, or `null`.
Country code.State or region.City.Street address.Second address line.Postal code.Phone number.
## Country
Available as `user.country`.
Localized country name.Two-letter code.Three-letter code.Flag image URL.
## Workspace invitation
Entries of `workspace.invitations`.
UUID.Invited address.Unix timestamp.Unix timestamp, or `null`.
## Notes on types
* **Timestamps** are Unix seconds, or `null`. Format them with Twig's date filters.
* **Money** is an integer in the currency's minor units. Use `` or `format_currency`.
* **Credits** use `null` for unlimited, which is different from `0`.
* **Percentages** on a subscription are strings that already include `%`.
## Related
* [Twig reference](/development/themes/twig-reference)
* [Pricing and plans](/development/themes/pricing-and-plans)
* [Users and navigation](/development/themes/users-and-navigation)
# Templates and layouts
Source: https://docs.aikeedo.com/development/themes/templates-and-layouts
Compose Aikeedo theme pages with Twig layouts, sections and snippets, and wire them to the site settings administrators control.
Theme templates are plain Twig. The application renders `@theme/templates/index.twig` for the landing page and hands it a set of globals; how you organize everything below that is up to you.
## The layout
A layout owns the HTML shell and declares the blocks pages fill in:
```twig layouts/theme.twig theme={null}
{% if env.THEME_ASSETS_SERVER is defined and env.THEME_ASSETS_SERVER %}
{% endif %}
{% include "@theme/snippets/css.twig" %}
{%- include "@theme/snippets/script-tags/head.twig" -%}
{% block title %}{% endblock %}
{{- block('title') is empty ? '' : ' | ' -}}
{{- option.site.name ?? '' -}}
{%- include "@theme/snippets/script-tags/body.twig" -%}
{% block template %}{% endblock %}
{%- include "@theme/snippets/script-tags/end.twig" -%}
```
The Vite client tag is what makes hot reloading work, and it only appears while `THEME_ASSETS_SERVER` is set.
## Pages
```twig templates/index.twig theme={null}
{% extends "@theme/layouts/theme.twig" %}
{% block title %}{{ dp__('theme', 'title', 'AI tools for teams') }}{% endblock %}
{% block template %}
{% include "@theme/sections/header.twig" %}
{% include "@theme/sections/hero.twig" %}
{% include "@theme/sections/features.twig" %}
{% include "@theme/sections/pricing.twig" %}
{% include "@theme/sections/footer.twig" %}
{% endblock %}
```
Everything resolves through `@theme`, which points at the active theme's directory.
## Sections and snippets
Sections are page-sized blocks. Snippets are fragments you pass data to:
```twig sections/pricing.twig theme={null}
{{ dp__('theme', 'heading', 'Pricing') }}
{% for plan in plans|default([]) %}
{% include "@theme/snippets/plan.twig" with { plan } %}
{% endfor %}
```
Dynamic includes work too, which is handy for icon sets or per-feature blocks:
```twig theme={null}
{% include "@theme/snippets/logos/" ~ provider ~ ".twig" ignore missing %}
```
## Strict variables
In debug mode Twig runs with strict variables: reading something undefined raises an error instead of rendering nothing. Most globals are conditional, so guard them.
```twig theme={null}
{% if user is defined %}
{{ dp__('theme', 'button', 'Open app') }}
{% endif %}
{{ option.site.name ?? '' }}
{{ plans|default([]) }}
{{ locale.code|default('en-US') }}
```
| Variable | Defined when |
| - | - |
| `user`, `workspace` | A visitor is signed in |
| `plans` | The template is the landing page |
| `locale` | The theme has a catalog for the resolved language |
| `option.*` | The setting has been saved at least once |
Test with debug mode on. A template that only breaks in production is usually a missing `is defined`.
## Honoring admin settings
The point of a theme is that it stays configurable. Read the settings administrators expect to control:
| Setting | Use |
| - | - |
| `option.site.name`, `.description`, `.keywords` | Title and meta tags |
| `option.brand.logo`, `.logo_dark`, `.favicon` | Header and favicon |
| `option.policies.tos`, `.privacy`, `.refund` | Footer links to `/policies/terms`, `/policies/privacy`, `/policies/refund` |
| `option.links.*` | Social links |
| `option.features.*.is_enabled` | Which product sections to show |
| `option.pwa.is_enabled` | Whether to link the web manifest |
| `option.color_scheme.*` | Light and dark mode, see [Dark mode and colors](/development/themes/dark-mode-and-colors) |
| `option.script_tags.*` | Analytics, see [Script tags](/development/themes/script-tags-and-analytics) |
```twig theme={null}
{% if option.brand.logo is defined and option.brand.logo %}
{% else %}
{% endif %}
```
## Translating
Theme strings belong to the `theme` domain:
```twig theme={null}
{{ d__('theme', 'Get started') }}
{{ dp__('theme', 'heading', 'Pricing') }}
```
A plain `__()` resolves against the application's catalog, not yours. See [Theme localization](/development/themes/localization).
## Markdown and formatting
Twig's Intl and Markdown extensions are available, which is useful for settings that hold rich text:
```twig theme={null}
{{ option.site.description|default('')|markdown_to_html }}
{{ plan.created_at|format_date(locale=locale.code|default('en-US')) }}
```
## Caching
Template caching is on when `CACHE=true` and `DEBUG=false`. During development keep caching off, and after deploying a template change on a cached installation, clear the cache from **Status → Clear cache**.
## Related
* [Template objects](/development/themes/template-objects)
* [Twig reference](/development/themes/twig-reference)
* [Pricing and plans](/development/themes/pricing-and-plans)
* [Custom pages](/development/themes/custom-pages)
# Theme troubleshooting
Source: https://docs.aikeedo.com/development/themes/troubleshooting
Fix the problems that come up most often when developing or installing an Aikeedo theme.
## The theme doesn't appear in the admin panel
The package failed validation, or it isn't where Aikeedo looks.
The directory is `extra/extensions/{vendor}/{name}` and matches the `name` field.
`type` is `aikeedo-theme`.
`require` includes `heyaikeedo/composer`.
`composer.json` is valid JSON.
Turn on `DEBUG=true` to see the validation error instead of a silently skipped package.
## Styles or scripts don't load
`npm run dev` must be running, and `THEME_ASSETS_SERVER` in the Aikeedo `.env` must match its port. The starter serves on 5174.
`extra.public` must include `assets` and `.vite`, and the theme must have been installed or reinstalled after the build so those files were copied to `public/e/{vendor}/{name}/`.
`THEME_ASSETS_SERVER` is still set. Remove it outside development.
## Template changes do nothing
* With `CACHE=true` and `DEBUG=false`, templates are cached. Clear the cache from **Status → Clear cache**.
* In development, the dev server copies `static/` into `BUILD_DIR`. If `BUILD_DIR` is wrong, the installed theme never changes.
* Check you're editing the repository, not the installed copy, which is overwritten by the next build.
## Twig errors about undefined variables
Debug mode enables strict variables:
```twig theme={null}
{% if user is defined %}…{% endif %}
{{ option.site.name ?? '' }}
{{ plans|default([]) }}
{{ locale.code|default('en-US') }}
```
`user` and `workspace` exist only for signed-in visitors, `plans` only on the landing page, and `locale` only when your theme has a catalog for the resolved language.
## The build deleted my files
The production build empties `BUILD_DIR` before writing. Point it at the theme's own directory, `extra/extensions/{vendor}/{name}`, and nothing else.
## A custom page returns 404
The theme is published, since a theme's entry class only boots while it's active.
The package was installed with `composer require`, so the class autoloads.
`AttributeMapper::addPath()` covers the handler's directory.
The route cache was cleared, or `CACHE=false`.
## Strings aren't translated
The string uses `d__('theme', …)` or `dp__('theme', …)`, not a plain `__()`.
A `theme.po` exists for that language, with `X-Domain: theme`.
The language is enabled in **Settings → Languages**.
Extraction was re-run after the last copy change.
## The favicon 404s
`{{ 'favicon.ico'|asset_url }}` resolves to your theme's assets, so either ship that file or fall back to the brand setting:
```twig theme={null}
```
## Dark mode flashes the wrong colors
The mode must be applied in the ``, before the body paints. See [Dark mode and colors](/development/themes/dark-mode-and-colors).
## The site shows the old theme after publishing
Clear the cache, and check that the theme's package name matches its directory. Preview separately at `/preview?theme=vendor/name`, which sets a short-lived cookie rather than switching the live site.
## Analytics stopped working after switching themes
Your theme has to render the script-tag snippets. Without them the codes configured in the admin panel never load. See [Script tags and analytics](/development/themes/script-tags-and-analytics).
## Where to look
| Source | What it tells you |
| - | - |
| `var/log/app-*.log`, `var/log/error-*.log` | PHP errors, including template failures |
| Browser console and network tab | Missing assets, JavaScript errors |
| The Vite terminal | Build failures |
| **Status** in the admin panel | Cache clearing, and installation health |
## Related
* [Build and assets](/development/themes/build-and-assets)
* [Theme quickstart](/development/themes/quickstart)
* [Local development](/development/local-development)
# Twig reference
Source: https://docs.aikeedo.com/development/themes/twig-reference
Every Twig function, filter, test and global available in an Aikeedo theme template.
Theme templates run in the application's Twig environment, so they get its extensions on top of standard Twig.
## Theme assets
Resolves a theme asset to a URL: a full URL is returned unchanged, then the dev server is used when `THEME_ASSETS_SERVER` is set, then the Vite manifest, then `/e/{theme}/assets/...`.
```twig theme={null}
```
Resolves an **application** asset through the app's own build manifest. Themes rarely need it; use `asset_url` for your own files.
## Translation
All nine gettext helpers are available as both functions and filters. Themes use the `theme` domain, so reach for the `d`-prefixed variants.
| Helper | Signature | Use |
| - | - | - |
| `d__` | `d__(domain, text)` | A theme string |
| `dp__` | `dp__(domain, context, text)` | A theme string with a context |
| `dn__` | `dn__(domain, singular, plural, count)` | A theme plural |
| `dnp__` | `dnp__(domain, context, singular, plural, count)` | A theme plural with a context |
| `__` | `__(text)` | A string from the application's own catalog |
| `p__` | `p__(context, text)` | An application string with a context |
| `n__` | `n__(singular, plural, count)` | An application plural |
| `np__` | `np__(context, singular, plural, count)` | An application plural with a context |
| `noop__` | `noop__(text)` | Marks a string for extraction without translating it here |
```twig theme={null}
{{ d__('theme', 'Get started') }}
{{ dp__('theme', 'heading', 'Pricing') }}
{{ dn__('theme', '%d seat', '%d seats', count)|format(count) }}
```
A plain `__()` in a theme looks up the application's catalog, not yours, so your translation won't be found and the string won't be extracted into your catalogs.
## Other functions
`hash_hmac(algo, data, key)`. Used for identity verification in support widgets. It runs server-side, so the secret stays out of the page.
`locale_script(code)` returns the URL of the browser translation catalog for a language, or null. Include it when your JavaScript needs translated strings.
`template(string, name = null, strict = false)` compiles a string into a template. It's how administrator-supplied markup is rendered; strict variables are disabled, and unparsable input falls back to being printed verbatim.
```twig theme={null}
{{ include(template(option.script_tags.custom.head))|raw }}
```
## Tests
True when an object's class is marked as built into the application, which the admin templates use to distinguish bundled integrations from plugin-provided ones.
## Extensions inherited from Twig
| Source | Examples |
| - | - |
| `twig/intl-extra` | `format_currency`, `format_number`, `format_date`, `format_datetime`, `country_name`, `currency_symbol` |
| `twig/markdown-extra` | `markdown_to_html`, `html_to_markdown` |
| Twig core | `escape`, `raw`, `json_encode`, `date`, `filter`, `map`, `merge`, `default`, and the rest |
```twig theme={null}
{{ plan.price / 100|format_currency(currency.code) }}
{{ option.site.description|default('')|markdown_to_html }}
```
## Globals
| Variable | Contents |
| - | - |
| `option` | Every setting, as a nested object |
| `config` | Application configuration, including the model registry |
| `version` | The application version |
| `license` | The license string, or null |
| `theme` | The active theme's package name |
| `environment` | `prod`, `dev`, `demo` or `install` |
| `nav` | The navigation registry |
| `currency` | Code, name, symbol and fraction digits |
| `locales` | Every configured language |
| `locale` | The language resolved for this request, or null |
| `theme_locale` | Deprecated. The language code as a string. Use `locale.code`. |
| `env` | The raw environment |
| `user`, `workspace` | Only when a visitor is signed in |
| `plans` | Only on the landing page |
See [Template objects](/development/themes/template-objects) for the full shape of each object.
`env` exposes the application's environment variables, including database credentials and secret keys. Read one specific key when you must, such as `env.THEME_ASSETS_SERVER`, and never print or serialize the whole object.
## Namespaces
| Prefix | Points at |
| - | - |
| `@theme/...` | The active theme's directory. Use it for every include and extend in your theme. |
| `/templates/...`, `/layouts/...` | Application templates. Themes shouldn't depend on these. |
| `@emails/...` | Email templates |
## Strict variables
With `DEBUG=true`, Twig raises an error for undefined variables. Guard optional values with `is defined` or `|default(...)`, and test your theme in debug mode before release.
## Related
* [Template objects](/development/themes/template-objects)
* [Templates and layouts](/development/themes/templates-and-layouts)
* [Theme localization](/development/themes/localization)
# Users and navigation
Source: https://docs.aikeedo.com/development/themes/users-and-navigation
Build a theme header that adapts to signed-in visitors, links into the app, and lets people switch language.
The public site is seen by guests and by signed-in users. A good header tells them apart, and gets people into the application quickly.
## Guest or signed in
```twig sections/header.twig theme={null}
{% if option.brand.logo is defined and option.brand.logo %}
{% else %}
{% endif %}
```
`user` is only defined for signed-in visitors, and debug mode errors on undefined variables, so the `is defined` check isn't optional.
## Links into the application
| Destination | Path |
| - | - |
| The app | `/app` |
| Account settings | `/app/settings/account` |
| Billing | `/app/billing` |
| Checkout for a plan | `/app/billing/checkout/{plan_id}` |
| Sign in | `/login` |
| Sign up | `/signup` |
| Sign out | `/logout` |
| Terms, privacy, refunds | `/policies/terms`, `/policies/privacy`, `/policies/refund` |
Policy pages exist only when the administrator filled them in, so link conditionally:
```twig theme={null}
{% if option.policies.tos is defined and option.policies.tos %}
{{ d__('theme', 'Terms of service') }}
{% endif %}
```
## Showing workspace details
For a signed-in visitor you can show the current workspace, its plan and its credits:
```twig theme={null}
{% if user is defined and workspace is defined %}
{{ workspace.name }}
{% if workspace.subscription %}
{{ workspace.subscription.plan.title }}
{% endif %}
{% endif %}
```
Keep this light. The landing page is a marketing page, and account details belong in the app.
## The language switcher
Languages come from `locales`, and the current one from `locale`. Each enabled language is reachable at a URL prefix:
```twig theme={null}
{% if locales|filter(l => l.enabled)|length > 1 %}
{% endif %}
```
Setting the cookie is what makes the choice stick on pages without a prefix.
### How the language is resolved
`/de-DE/...` wins, if that language is enabled.
Their account preference.
What the switcher set.
The browser's preference.
Configured in the admin panel.
Themes register routes with an optional `[locale:locale]?` segment so their pages work under a prefix. See [Custom pages](/development/themes/custom-pages).
## Right-to-left languages
```twig theme={null}
```
Use logical CSS properties, such as Tailwind's `ms-*`, `me-*`, `ps-*` and `pe-*`, rather than left and right, so an RTL language lays out correctly.
## Social and footer links
```twig theme={null}
```
## Related
* [Template objects](/development/themes/template-objects)
* [Theme localization](/development/themes/localization)
* [Templates and layouts](/development/themes/templates-and-layouts)
# Mailer Configuration
Source: https://docs.aikeedo.com/email/mailer
Configure email delivery in Aikeedo using SMTP or Null transport, set sender details, and test email functionality for user communications.
Proper email configuration ensures reliable delivery of system notifications, password resets, and user communications. This guide covers mailer transport setup and sender configuration.
## Understanding Mailer Settings
Before diving into the configuration process, it's important to understand the key components of Aikeedo's mailer system.
### Mailer Transports
Aikeedo supports two types of mailer transports:
1. **Null Transport (Default)**
* This option ignores sending emails, making it useful for testing purposes.
* No actual emails are sent when this transport is selected.
2. **SMTP Transport**
* Sends emails using an SMTP (Simple Mail Transfer Protocol) server.
* Requires additional configuration of SMTP settings.
If you choose the SMTP Transport, make sure to configure your SMTP settings first. You can find detailed instructions in our [SMTP Configuration Guide](/email/smtp).
### Sender Details
Two important fields define how your emails appear to recipients:
* **Sender Email**: The email address that will be used as the sender for all system emails.
* **Sender Name**: The name that will appear as the sender for all system emails.
## Step-by-Step Configuration Guide
Follow these steps to set up your mailer in Aikeedo:
### 1. Access the Admin Panel
To access the mailer configuration in Aikeedo, log in to the admin panel using your administrator credentials. Once logged in, navigate to Settings > Email > Mail. This will take you to the mailer configuration page where you can adjust your email settings.
### 2. Configure Mailer Settings
1. In the "General" section, find the "Mailer transport" dropdown menu.
2. Choose between "Null Transport" (for testing) or "SMTP Transport" (for actual email sending).
If you select "SMTP Transport", ensure you have properly configured your SMTP settings beforehand.
1. Locate the "Sender details" section.
2. Enter the desired email address in the "Sender email" field (e.g., [noreply@yourdomain.com](mailto:noreply@yourdomain.com)).
3. Specify the name you want recipients to see in the "Sender name" field (e.g., "Aikeedo Support").
### 3. Save Your Configuration
After entering all the necessary information:
1. Review your settings to ensure accuracy.
2. Click the "Save changes" button at the bottom of the page to apply your new mail configuration.
Your mailer is now configured! Aikeedo will use these settings for all system-generated emails.
## Testing Your Configuration
It's always a good idea to test your email configuration to ensure everything is working correctly:
1. Set up a test user account or use an existing one.
2. Trigger an email-sending action (e.g., password reset, welcome email).
3. Check the receiving inbox to confirm the email was sent and received correctly.
If you're using the Null Transport for testing, remember that no actual emails will be sent. You may need to check your application logs or use a debugging tool to verify the email sending process.
## Troubleshooting
If you encounter issues with email delivery:
* Verify your SMTP settings if using SMTP Transport
* Ensure your sender email is correctly formatted and from a valid domain
* Check server firewall settings for outgoing SMTP traffic
* Consider using a dedicated email service provider for improved deliverability
* Review server logs for detailed error messages
For advanced configuration, refer to our [SMTP Configuration Guide](/email/smtp).
## Related Guides
* [SMTP Setup](/email/smtp) - Configure SMTP server details
* [Email Templates](/advanced/email-templates) - Customize email content
* [Application Configuration](/setup/configuration) - Complete setup checklist
# SMTP Configuration
Source: https://docs.aikeedo.com/email/smtp
Learn how to set up and configure SMTP for secure and reliable email delivery in Aikeedo.
# SMTP Configuration
Setting up SMTP (Simple Mail Transfer Protocol) is crucial for ensuring reliable email delivery from your Aikeedo application. This guide will walk you through the process of configuring SMTP settings, whether you're a tech-savvy administrator or new to email server configurations.
## Understanding SMTP
SMTP is the standard protocol for sending emails across the Internet. By configuring SMTP in Aikeedo, you ensure that all system-generated emails (such as user notifications, password resets, and marketing communications) are sent reliably and securely.
## Before You Begin
Before configuring SMTP settings, you'll need to obtain the following details from your SMTP provider:
* SMTP Host (server address)
* Port number
* Username
* Password
* Encryption method (TLS/SSL)
If you don't have an SMTP server, consider using a reputable third-party service like [SendGrid](https://sendgrid.com/), [Mailgun](https://www.mailgun.com/), [Amazon SES](https://aws.amazon.com/ses/), or [Brevo](https://www.brevo.com/). These services offer reliable email delivery and often provide free tiers for low-volume senders.
## Step-by-Step Configuration
Follow these steps to set up SMTP in your Aikeedo application:
### 1. Access the Admin Panel
Log in to your Aikeedo admin panel using your administrator credentials.
### 2. Navigate to SMTP Settings
In the admin panel, follow this path:
**Settings > Email > SMTP**
### 3. Enter SMTP Details
Fill in the form with the SMTP details you obtained from your provider:
* **SMTP Host**: Enter the address of your SMTP server (e.g., `smtp.gmail.com` for Gmail)
* **Port**: Specify the port number (common ports are 25, 465, or 587)
* **Username**: Your SMTP server username (often your email address)
* **Password**: Your SMTP server password or app-specific password
For enhanced security, many providers recommend using app-specific passwords instead of your main account password. Check your email provider's documentation for instructions on generating these.
### 4. Save and Validate
Click the "Save changes" button to apply your new SMTP settings. Aikeedo will automatically attempt to validate your configuration before saving.
### 5. Test the Configuration
After saving, it's a good practice to send a test email to ensure everything is working correctly:
1. Set up a test user account or use an existing one.
2. Trigger an email-sending action (e.g., password reset, welcome email).
3. Check the receiving inbox to confirm the email was sent and received correctly.
## cPanel SMTP Restrictions
If your Aikeedo installation is on a cPanel server, you may encounter issues when trying to use external SMTP servers. This is because cPanel blocks access to external SMTP servers by default as a security measure.
### Resolving cPanel SMTP Restrictions
If you're experiencing difficulties sending emails through an external SMTP server on a cPanel-managed hosting account, follow these steps:
1. Contact your hosting provider or server administrator.
2. Request that they disable SMTP restrictions for your account.
3. The server administrator can disable SMTP restrictions through WHM (Web Host Manager) by navigating to:
**Home » Security Center » SMTP Restrictions**
4. On this page, they can disable the SMTP restrictions for your specific account or globally for the server.
For more information on this process, you can refer to the [cPanel documentation on disabling SMTP restrictions](https://support.cpanel.net/hc/en-us/articles/1500009931961-How-to-disable-SMTP-Restrictions).
## Troubleshooting
If you encounter issues with your SMTP configuration, try the following:
* Double-check all entered information for typos or errors
* Ensure your SMTP provider account is in good standing
* Check if your server's IP is blacklisted
* Verify that your firewall isn't blocking the necessary ports
* If using cPanel, confirm that SMTP restrictions have been disabled
If you're using a personal email account (like Gmail) for SMTP, you may need to enable "Less secure app access" or use an app-specific password. However, for production environments, it's recommended to use a dedicated SMTP service.
## Next Steps
Once you've successfully configured SMTP, make sure to update your [Mail Settings](/email/mailer) to use SMTP as the transport method. This ensures that all outgoing emails from Aikeedo will use your new SMTP configuration.
## Security Considerations
* Regularly update your SMTP password to maintain security
* Monitor your email sending patterns to detect any unusual activity
* Consider implementing SPF, DKIM, and DMARC records to improve email deliverability and security
By following this guide, you should now have a properly configured SMTP setup for your Aikeedo application, ensuring reliable email delivery for all your users and system-generated communications.
# AI/ML API Integration
Source: https://docs.aikeedo.com/integrations/aimlapi
Enhance your Aikeedo platform with AI/ML API's advanced language models. This guide walks you through setting up and leveraging AI/ML API's powerful Chirp models for various AI tasks.
**Integration Status: Removed in v5.0**
This integration existed to power the Composer (music) tool through the Suno API, which was discontinued on November 27, 2024. Composer and this integration were removed in Aikeedo v5.0, and existing compositions are deleted when you update. See the [v5.0 release notes](/versioning/release-notes#composer-removed).
AI/ML API language models can still be added as a custom LLM server. See [OpenAI API Compatibility](/integrations/openai-api-compatibility). The rest of this page applies to v4.x and earlier only.
## Introduction
Aikeedo's AI/ML API integration unlocks advanced language processing capabilities for your platform. With access to the Chirp models, you can significantly enhance your AI-powered tools, allowing users to perform a wide range of language-related tasks.
## Setting Up the Integration
Follow these steps to connect AI/ML API with your Aikeedo platform:
### Step 1: Obtain Your AI/ML API Key
1. If you don't have an AI/ML API account, [sign up here](https://aimlapi.com/).
2. Once logged in, navigate to your AI/ML API dashboard.
3. In the left sidebar, click on "API Keys" under the "Key Management" section.
4. If you don't have an existing API key, you'll need to generate a new one.
5. Copy your API key—you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to AI/ML API services and should not be shared publicly.
AI/ML API provides an AI Playground where you can test different models and settings. This can be useful for experimenting with the Chirp models before integrating them into your Aikeedo platform.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > AI/ML API**.
3. In the "API" section, enter your AI/ML API key in the "API Key" field.
4. Click **Save changes** to activate the integration.
Great job! AI/ML API is now integrated with your Aikeedo platform.
## Aikeedo Tools Powered by AI/ML API
AI/ML API integration enhances the following Aikeedo tool:
1. **Composer**: Create, edit, and refine text content using AI/ML API's Chirp language models.
## Best Practices
To make the most of your AI/ML API integration:
* Experiment with both Chirp models to find the best fit for your use case.
* Provide clear and detailed prompts for optimal results.
* Consider the complexity and context of your tasks when selecting between v3.5 and v3.0.
* Monitor your API usage to manage costs effectively.
* Stay updated on any new features or improvements to the Chirp models.
## Troubleshooting
If you encounter issues with the AI/ML API integration, try these steps:
1. Verify that your API key is correct and active.
2. Check your AI/ML API account for any usage limits or restrictions.
3. Ensure your Aikeedo platform is up to date.
4. If you receive error messages related to model access or quotas, verify your account status with AI/ML API.
5. For model-specific issues, confirm that your account has access to both Chirp models.
6. If you experience slow response times, consider optimizing your prompts or checking your network connection.
Can't find your API Key? Click the "Click here" link below the API Key field in the Aikeedo settings for additional guidance.
Remember to review AI/ML API's usage policies and pricing to ensure compliance and manage costs effectively as you scale your AI integration.
# Azure Integration
Source: https://docs.aikeedo.com/integrations/azure
Enhance your Aikeedo platform with Microsoft Azure's advanced AI capabilities. This guide walks you through setting up and leveraging Azure's powerful text-to-speech services for high-quality voice generation.
## Introduction
Aikeedo's Azure integration unlocks advanced text-to-speech capabilities for your platform. With access to Azure's Speech API, you can significantly enhance your VoiceOver tool and audio content creation with high-quality, natural-sounding voices.
## Setting Up the Integration
Follow these steps to connect Microsoft Azure with your Aikeedo platform:
### Step 1: Set Up Azure Speech Service
1. If you don't have an Azure account, [sign up here](https://azure.microsoft.com/free/).
2. Once logged in, access the Azure portal at [portal.azure.com](https://portal.azure.com).
3. Create a new Speech service resource:
* Click on "Create a resource" in the Azure portal.
* Search for "Speech" and select "Speech" from the results.
* Click "Create" to start setting up the Speech service.
* Fill in the required details:
* Choose a name for your resource.
* Select your subscription.
* Choose a resource group or create a new one.
* Select a region (this will be your "Location/Region" in Aikeedo).
* Choose a pricing tier (Free tier is available for testing).
* Click "Review + create" and then "Create" to deploy the Speech service.
4. Once deployment is complete, go to your Speech service resource.
5. In the left menu, under "Resource Management", click on "Keys and Endpoint".
6. You'll find your API key (listed as KEY 1 or KEY 2) and region here. You'll need these for Aikeedo.
Keep your API key secure and confidential. It grants access to your Azure services and should not be shared publicly.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > Azure Integrations**.
3. In the "Speech API (TTS)" section:
* Select your "Location/Region" from the dropdown menu.
* Enter your Azure Speech API key in the "API Key" field.
4. Click **Save changes** to activate the integration.
Great job! Azure Speech API is now integrated with your Aikeedo platform.
## Available Voice Types
Azure's Text-to-Speech service offers a variety of voice types:
* Neural voices
* Standard voices
* Custom voices (if configured in your Azure account)
These voice types offer different levels of quality and naturalness, allowing you to choose the best fit for your specific use case.
## Aikeedo Tools Powered by Azure
Azure integration enhances the following Aikeedo tool:
1. **VoiceOver**: Transform text into high-quality, natural-sounding speech using a wide range of voices and languages.
## Best Practices
To make the most of your Azure integration:
* Experiment with different voice types to find the best fit for your use case.
* Consider the language and accent requirements of your project when selecting voices.
* Monitor your API usage to manage costs effectively.
## Troubleshooting
If you encounter issues with the Azure integration, try these steps:
1. Verify that your API key is correct and active.
2. Ensure you've selected the correct region in Aikeedo that matches your Azure Speech service resource.
3. Check your Azure subscription for any usage limits or restrictions.
4. Confirm you have sufficient credit balance in your Azure account.
5. For voice-specific issues, verify that your account has access to the required voice types.
6. If you receive error messages related to authentication or API calls, double-check your Azure resource configuration.
Regularly review your Azure usage and available voice options to ensure smooth integration with Aikeedo.
Remember to review Microsoft Azure's usage policies and pricing to ensure compliance and manage costs effectively as you scale your text-to-speech integration.
# Claude AI Integration
Source: https://docs.aikeedo.com/integrations/claude
Integrate Anthropic Claude AI models (Claude Sonnet, Opus, Haiku) into Aikeedo for advanced chat, Canvas documents, and content generation.
## Introduction
Claude AI integration provides access to Anthropic's advanced language models, known for strong performance in content generation, coding assistance, and complex reasoning tasks.
## Setting Up the Integration
Follow these steps to connect Claude AI with your Aikeedo platform:
### Step 1: Obtain Your Claude AI API Key
1. If you don't have an Anthropic account, [sign up here](https://www.anthropic.com) or contact their sales team for API access.
2. Once you have access, navigate to your Anthropic dashboard.
3. Locate and copy your API key—you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to Claude AI
services and should not be shared publicly.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > Anthropic / Claude AI**.
3. Paste your Claude AI API key into the "API Key" field.
4. Click **Save changes** to activate the integration.
Great job! Claude AI is now integrated with your Aikeedo platform.
## Custom API Keys (BYOK)
Aikeedo supports Bring Your Own Key (BYOK) functionality for Claude AI integration:
* **Workspace-level API keys**: Workspace owners can provide their own Claude AI API key
* **System override**: When configured, the workspace-specific API key takes precedence over the system-wide API key
* **Credit management**: Custom API keys bypass workspace credit deductions for Claude AI services
* **Subscription requirement**: A valid subscription plan and credit balance are still required for other features and API services
BYOK is particularly useful for organizations that want to manage their own
Claude AI billing and usage directly. For detailed information about BYOK
configuration and setup, see the [BYOK documentation](/advanced/byok).
## Aikeedo Tools Powered by Claude AI
Claude AI integration enhances the following Aikeedo tools:
1. **Chat**: Engage in sophisticated, context-aware conversations, with Canvas documents and clarifying questions.
2. **Writer**: Answer a template's questions and continue the result in chat.
3. **Utilities**: Generate titles and enhance prompts.
## Best Practices
To maximize your Claude AI integration:
* Experiment with different models to find the best balance of performance and efficiency for your use case.
* Provide clear, detailed instructions to get the most accurate and relevant outputs.
* Leverage Claude's ability to handle multi-step tasks by breaking down complex problems.
* Regularly review and update your usage to align with Anthropic's evolving best practices.
## Troubleshooting
If you encounter issues with the Claude AI integration:
1. **API Key Issues**:
* Verify that your API key is correct and active
* Check your Anthropic account for any usage limits or restrictions
* Ensure your Aikeedo platform is up to date
2. **Model Access and Quota Issues**:
* If you receive error messages related to model access or quotas, verify your account status with Anthropic
* Ensure your Anthropic account has sufficient credits and the correct billing setup for the models you're trying to use
3. **BYOK Issues**:
* If using custom API keys, ensure the workspace-specific key is valid and active
* Verify that the custom key has the necessary permissions for required models
* For comprehensive BYOK troubleshooting and configuration details, refer to the [BYOK documentation](/advanced/byok)
Remember to review Anthropic's usage policies and pricing to ensure compliance
and manage costs effectively as you scale your Claude AI integration.
# Clipdrop Integration
Source: https://docs.aikeedo.com/integrations/clipdrop
Enhance your Aikeedo platform with Clipdrop's advanced image generation capabilities. This guide walks you through setting up and leveraging Clipdrop's powerful AI models for creating unique and high-quality images.
## Important Notice
Clipdrop uses Stability AI under the hood for its image generation capabilities. For optimal performance and direct access to the latest features, we highly recommend using the Stability AI integration instead of Clipdrop.
Learn how to set up and use Stability AI directly in Aikeedo for advanced image generation.
## Introduction
Aikeedo's Clipdrop integration unlocks advanced image generation capabilities for your platform. With access to Clipdrop's cutting-edge AI models, you can significantly enhance your Imagine tool, allowing users to create unique and high-quality images from text descriptions.
## Setting Up the Integration
Follow these steps to connect Clipdrop with your Aikeedo platform:
### Step 1: Obtain Your Clipdrop API Key
1. If you don't have a Clipdrop account, [sign up here](https://clipdrop.co/).
2. Once logged in, navigate to your Clipdrop dashboard.
3. Look for the API section or API key management area.
4. Generate a new API key if you don't already have one.
5. Copy your API key—you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to Clipdrop services and should not be shared publicly.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > Clipdrop**.
3. In the "API" section, enter your Clipdrop API key in the "API Key" field.
4. Click **Save changes** to activate the integration.
Great job! Clipdrop is now integrated with your Aikeedo platform.
## Aikeedo Tools Powered by Clipdrop
Clipdrop integration enhances the following Aikeedo tool:
1. **Imagine**: Create unique and high-quality images from text descriptions using Clipdrop's advanced AI models.
## Best Practices
To make the most of your Clipdrop integration:
* Provide clear and detailed text prompts for optimal image generation results.
* Experiment with different prompt styles to find what works best for your needs.
* Consider the style and complexity requirements of your project when crafting prompts.
* Monitor your API usage to manage costs effectively.
* Regularly update your integration to access the latest features and improvements.
## Troubleshooting
If you encounter issues with the Clipdrop integration, try these steps:
1. Verify that your API key is correct and active.
2. Check your Clipdrop account for any usage limits or restrictions.
3. Ensure your Aikeedo platform is up to date.
4. If you receive error messages related to API access or quotas, verify your account status with Clipdrop.
5. If you experience slow response times, consider optimizing your prompts or checking your network connection.
Regularly review your Clipdrop usage to ensure smooth integration with Aikeedo.
Remember to review Clipdrop's usage policies and pricing to ensure compliance and manage costs effectively as you scale your image generation integration.
# AWS S3
Source: https://docs.aikeedo.com/integrations/cloud-storage/aws-s3
Configure AWS S3 cloud storage for file uploads and AI-generated content in Aikeedo. Industry-standard object storage with high durability and availability.
## Overview
AWS S3 (Simple Storage Service) is Amazon's industry-standard object storage service, offering high durability, availability, and scalability. It's one of the most popular choices for cloud storage due to its reliability and extensive feature set.
**Key features:**
* **High durability** - 99.999999999% (11 9's) durability
* **Global availability** - Multiple regions worldwide
* **Scalable storage** - Virtually unlimited storage capacity
* **Security features** - Encryption, access controls, and compliance
* **Integration ecosystem** - Works with thousands of AWS services
* **S3-compatible API** - Easy integration with existing tools
## Prerequisites
Before configuring AWS S3:
1. **AWS Account** - Create an account at [aws.amazon.com](https://aws.amazon.com)
2. **S3 Bucket** - Create a bucket for your files
3. **IAM User** - Create a user with S3 permissions
4. **API Credentials** - Generate access key and secret key
5. **Domain** - Optional custom domain for CloudFront CDN
## Configuration Steps
In your AWS Console:
1. Go to [**S3** service](https://console.aws.amazon.com/s3/home)
2. Select your preferred region from the region selector in the top-right corner
3. Click **Create bucket**
4. Choose a unique bucket name (e.g., `your-app-storage`)
5. Use default configuration (sufficient for most use cases)
6. Click **Create bucket**
Bucket names must be globally unique across all AWS accounts. Names must be 3-63 characters long, contain only lowercase letters, numbers, dots, and hyphens, and cannot start or end with a dot or hyphen.
Create an IAM user with S3 permissions:
1. Go to **IAM** > **Users** > **Create user**
2. Enter username (e.g., `aikeedo-storage`)
3. Leave **"Provide user access to the AWS Management Console"** unchecked (programmatic access only)
4. Click **Next**
5. Select **"Attach policies directly"**
6. Search for and select **AmazonS3FullAccess** policy
7. Click **Next** to review, then **Create user**
8. Go to the user's **Security credentials** tab
9. Click **Create access key**
10. Select **"Application running outside AWS"** as the use case
11. Click **Next** to proceed
12. Copy the **Access key ID** and **Secret access key**
13. Save credentials securely
Set up CORS for your domain:
1. Go to your S3 bucket
2. Click **Permissions** tab
3. Scroll to **Cross-origin resource sharing (CORS)**
4. Click **Edit** and add this configuration:
```json theme={null}
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["https://yourdomain.com", "https://www.yourdomain.com"]
}
]
```
5. Click **Save changes**
Replace `yourdomain.com` with your actual domain. For development, you can use `*` for AllowedOrigins, but this is not recommended for production.
In your admin panel:
1. Go to **Settings** > **Cloud storage** > **AWS S3**
2. Toggle **Status** to **Enabled**
3. Enter **Endpoint**:
* Go to your S3 bucket in AWS Console
* Copy the **Bucket website endpoint** or use format: `https://your-bucket.s3.region.amazonaws.com/`
* For example: `https://my-app-storage.s3.us-east-1.amazonaws.com/`
4. Select **Region** from dropdown (must match your bucket's region)
5. Enter **Custom domain** (optional CloudFront CDN domain)
6. Enter **Bucket name**: Your S3 bucket name
7. Enter **Path prefix** (optional subfolder)
8. Enter **Access key**: Your IAM access key ID
9. Enter **Secret key**: Your IAM secret access key
10. Click **Save changes**
Enable AWS S3 as your storage provider:
1. Go to **Settings** > **File storage**
2. In the **Adapter** dropdown, select **AWS S3**
3. Configure **Group files** and **Secure URLs** as needed
4. Click **Save changes**
## Configuration Fields
**Required fields:**
* **Endpoint** - S3 bucket endpoint URL (e.g., `https://your-bucket.s3.region.amazonaws.com/`)
* **Region** - AWS region where your bucket is located
* **Bucket name** - Name of your S3 bucket
* **Access key** - IAM user access key ID for authentication
* **Secret key** - IAM user secret access key for authentication
**Optional fields:**
* **Custom domain** - Custom domain for CloudFront CDN (if you have CloudFront distribution set up)
* **Path prefix** - Optional subfolder within your bucket for organizing files
## Troubleshooting
**Access denied errors:**
* Verify IAM user has correct permissions for the specific bucket
* Check bucket policy settings and public access settings
* Ensure access keys are valid and not expired
* Verify bucket name matches exactly (case-sensitive)
**CORS issues:**
* Check CORS configuration in S3 bucket permissions
* Verify allowed origins include your domain (with https\://)
* Ensure allowed methods include GET (sufficient for Aikeedo)
* Wait a few minutes for CORS changes to propagate
**Files not uploading:**
* Verify bucket name and region are correct
* Check network connectivity to AWS
* Ensure bucket exists and is accessible
* Verify endpoint URL format is correct
* Check if bucket has any restrictions or policies blocking uploads
**Performance issues:**
* Consider using CloudFront CDN for better global performance
* Check if you're using the correct region for your users
* Monitor AWS CloudWatch for any service issues
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Cloudflare R2
Source: https://docs.aikeedo.com/integrations/cloud-storage/cloudflare-r2
Configure Cloudflare R2 cloud storage for file uploads and AI-generated content in Aikeedo. S3-compatible storage with zero egress fees.
## Overview
Cloudflare R2 is an S3-compatible object storage service that offers zero egress fees, making it cost-effective for applications with high data transfer requirements. It's integrated with Cloudflare's global network for optimal performance.
## Prerequisites
Before configuring Cloudflare R2:
1. **Cloudflare Account** - Create an account at [cloudflare.com](https://cloudflare.com)
2. **R2 Bucket** - Create a bucket for your files
3. **API Token** - Generate R2 API credentials
4. **Domain** - Optional custom domain setup
## Configuration Steps
In your Cloudflare dashboard:
1. Go to **R2 Object Storage** in the left sidebar
2. Click **Create bucket**
3. Enter a unique bucket name (e.g., `your-app-storage`)
4. Choose location:
* **Automatic** (recommended) - Cloudflare chooses optimal location
* **Specify jurisdiction** - For data residency requirements
5. Select storage class:
* **Standard** (recommended) - For objects accessed at least once a month
* **Infrequent Access** - For objects accessed less than once a month
6. Click **Create bucket**
Bucket names must be globally unique across all Cloudflare accounts and are permanent once created. Choose a name that reflects your application.
By default, buckets are not publicly accessible. You can access objects through the API or by binding the bucket to a Worker. Bucket access can be changed to Public at any time.
Create R2 API credentials:
1. Go to **R2 Object Storage** in the left sidebar
2. Click **Manage API tokens**
3. Choose token type:
* **Account API token** (recommended for production) - Tied to the account, remains active even when you leave the organization
* **User API token** (for development) - Tied to your user account, becomes inactive if you leave the organization
4. Click **Create Account API token** or **Create User API token**
5. Configure the token:
* **Token name**: Enter a descriptive name (e.g., `aikeedo-storage`)
* **Permissions**: Select **Object Read & Write** (allows read, write, and list objects in specific buckets)
* **Specify bucket(s)**: Choose **Apply to specific buckets only** and select your bucket
* **TTL**: Set to **Forever** or your preferred duration
6. Click **Create User API Token** (or **Create Account API Token**)
7. Copy the **Access Key ID** and **Secret Access Key** from the success page
8. Save credentials securely
You will not be able to access your Secret Access Key again after this step. Copy and record both values to avoid losing them.
The endpoint format is: `https://.r2.cloudflarestorage.com/`. You can copy this directly from your R2 bucket settings.
Set up CORS for your domain:
1. Go to your R2 bucket
2. Click **Settings** tab
3. Scroll to **CORS Policy** section
4. Click **Edit** button
5. In the JSON editor, configure the CORS policy:
```json theme={null}
[
{
"AllowedOrigins": [
"https://yourdomain.com",
"https://www.yourdomain.com"
],
"AllowedMethods": [
"GET"
]
}
]
```
6. Click **Save**
Replace `yourdomain.com` with your actual domain. For development, you can use `["*"]` for AllowedOrigins, but this is not recommended for production.
The CORS policy uses JSON format in Cloudflare R2. Make sure your JSON syntax is valid before saving.
In your admin panel:
1. Go to **Settings** > **Cloud storage** > **Cloudflare R2**
2. Toggle **Status** to **Enabled**
3. Enter **S3 API Endpoint**:
* Go to your R2 bucket settings in Cloudflare dashboard
* Copy the **S3 API** URL from the General section
* Paste it into the S3 API Endpoint field
4. Enter **Public access domain**:
* Use your custom domain (e.g., `https://cdn.yourdomain.com`) or R2.dev subdomain
* **Must include the `https://` protocol**
* If you've enabled Secure URLs, include the S3 API endpoint here
* Otherwise, URLs will be public
While Cloudflare R2 URLs are publicly accessible by design, Aikeedo ensures security by using hard-to-guess unique addresses for all uploaded files.
5. Enter **Bucket name**: Your R2 bucket name
6. Enter **Path prefix** (optional subfolder)
7. Enter **Access key ID**: Your R2 access key ID
8. Enter **Secret access key**: Your R2 secret access key
9. Click **Save changes**
Enable Cloudflare R2 as your storage provider:
1. Go to **Settings** > **File storage**
2. In the **Adapter** dropdown, select **Cloudflare R2**
3. Configure **Group files** and **Secure URLs** as needed
4. Click **Save changes**
## Configuration Fields
**Required fields:**
* **S3 API Endpoint** - R2 S3 API endpoint URL (copy from bucket settings General section)
* **Public access domain** - Public domain for accessing files (must include `https://` protocol, use custom domain or R2.dev subdomain)
* **Bucket name** - Name of your R2 bucket
* **Access key ID** - R2 access key ID for authentication
* **Secret access key** - R2 secret access key for authentication
**Optional fields:**
* **Path prefix** - Optional subfolder within your bucket for organizing files
## Troubleshooting
**Access denied errors:**
* Verify API token has correct permissions (Object Read & Write recommended)
* Check bucket name matches exactly
* Ensure S3 API endpoint format is correct: `https://.r2.cloudflarestorage.com/`
* Verify access keys are valid and not expired
* Check if token is scoped to the correct bucket
**CORS issues:**
* Check CORS configuration in bucket settings using the JSON editor
* Verify allowed origins include your domain (with https\://)
* Ensure allowed methods include GET (sufficient for Aikeedo)
* Validate JSON syntax is correct before saving
**Files not uploading:**
* Verify bucket name and endpoint are correct
* Check network connectivity to Cloudflare
* Ensure bucket exists and is accessible
* Verify API token permissions are sufficient
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Digital Ocean Spaces
Source: https://docs.aikeedo.com/integrations/cloud-storage/digital-ocean-spaces
Configure Digital Ocean Spaces cloud storage for file uploads and AI-generated content in Aikeedo. S3-compatible storage with simple pricing.
## Overview
Digital Ocean Spaces is an S3-compatible object storage service that offers simple pricing, easy setup, and global CDN integration. It's an excellent choice for developers who want AWS S3 compatibility without the complexity of AWS services.
**Key features:**
* **Predictable pricing** - Simple, transparent pricing model
* **Built-in CDN** - Global content delivery network included
* **Unlimited buckets** - Create up to 100 buckets per subscription
* **Custom subdomain** - Use your own domain for file delivery
* **S3-compatible API** - Easy integration with existing tools
## Prerequisites
Before configuring Digital Ocean Spaces:
1. **DigitalOcean Account** - Create an account at [digitalocean.com](https://digitalocean.com)
2. **Space** - Create a Space for your files
3. **API Keys** - Generate Spaces access keys
4. **Domain** - Optional custom domain for CDN
## Configuration Steps
In your DigitalOcean control panel:
1. Go to **Spaces Object Storage** in the left sidebar
2. Click **Create Bucket**
3. Choose a datacenter region (e.g., Frankfurt FRA1, San Francisco SFO3)
4. Optionally enable **CDN** for faster delivery
5. Enter a unique bucket name (e.g., `your-app-files`)
6. Select a project (e.g., `aikeedo`)
7. Click **Create a Spaces Bucket**
Bucket names must be globally unique across all DigitalOcean accounts. Names must be in lowercase, 3-63 characters long, and may contain dashes. **Cannot contain dots (.) or other special characters.**
Create API keys for Spaces access:
1. Go to **Spaces Object Storage** in the left sidebar
2. Click the **Access Keys** tab
3. Click **Generate New Key**
4. In the **Create Access Key** dialog:
* Select **Limited Access** (recommended) or **Full Access**
* If using Limited Access:
* Select specific buckets from the list
* Choose **Read/Write/Delete** permissions for each bucket (required for Aikeedo)
* Enter a name for the key (e.g., `aikeedo-access`)
* Note: Key names can only contain alphanumeric characters, dashes and periods
5. Click **Create Access Key**
6. Copy the **Access Key** and **Secret Key**
7. Save credentials securely
API keys provide access to your Spaces. Store them securely.
Set up CORS for your domain:
1. Go to your Space settings
2. Click **Settings** tab
3. Scroll to **CORS Configurations**
4. Click **Add a Rule** to open **Advanced CORS Options**
5. Configure the CORS settings:
* **Origin**: `https://yourdomain.com` (or `*` for all origins)
* **Allowed Methods**: Check `GET` (sufficient for Aikeedo)
* **Allowed Headers**: No special headers required for Aikeedo
* **Access Control Max Age**: `3000` (or `0` for no caching)
6. Click **Save CORS Configuration**
CDN and CORS configurations may not apply immediately. Wait a few moments and try again if you encounter issues.
In your admin panel:
1. Go to **Settings** > **Cloud storage** > **Digital Ocean Spaces**
2. Toggle **Status** to **Enabled**
3. Enter **Origin endpoint**:
* Go to your Space in DigitalOcean
* Copy the **Origin Endpoint** URL (e.g., `https://cdnaikeedocom.fra1.digitaloceanspaces.com`)
* Paste it into the Origin endpoint field
4. Enter **Custom domain** (optional CDN domain)
5. Enter **Bucket name**: Your Space name
6. Enter **Path prefix** (optional subfolder)
7. Enter **Access key**: Your Spaces access key
8. Enter **Secret key**: Your Spaces secret key
9. Click **Save changes**
Enable Digital Ocean Spaces as your storage provider:
1. Go to **Settings** > **File storage**
2. In the **Adapter** dropdown, select **Digital Ocean Spaces**
3. Configure **Group files** and **Secure URLs** as needed
4. Click **Save changes**
## Configuration Fields
**Required fields:**
* **Origin endpoint** - Spaces endpoint URL (e.g., `https://your-space.region.digitaloceanspaces.com`)
* **Bucket name** - Name of your DigitalOcean Space
* **Access key** - Spaces access key for authentication
* **Secret key** - Spaces secret key for authentication
**Optional fields:**
* **Custom domain** - Custom domain for CDN (if you have a custom domain for your Space)
* **Path prefix** - Optional subfolder within your Space for organizing files
## Troubleshooting
**Access denied errors:**
* Verify API keys are correct and active
* Check Space name matches exactly
* Ensure Space is not restricted
**CORS issues:**
* Check CORS configuration in Space settings
* Verify allowed origins include your domain
* Ensure allowed methods include required operations
**Files not uploading:**
* Verify region in endpoint URL
* Check network connectivity to DigitalOcean
* Ensure Space exists and is accessible
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# MinIO
Source: https://docs.aikeedo.com/integrations/cloud-storage/minio
Configure MinIO self-hosted object storage for file uploads and AI-generated content in Aikeedo. Complete control over your data with S3-compatible API.
## Overview
MinIO AIStor is a high-performance, S3-compatible object storage server that you can self-host. It gives you complete control over your data while maintaining compatibility with S3 APIs, making it perfect for private cloud deployments or when you need full data sovereignty.
**Key features:**
* **Self-hosted** - Complete control over your data and infrastructure
* **S3-compatible API** - Easy integration with existing S3 tools and libraries
* **High performance** - Optimized for speed and efficiency
* **Data sovereignty** - Keep your data on your own infrastructure
* **Enterprise features** - Advanced security, monitoring, and management
MinIO integration is available only if the Cloud Storage plugin is purchased from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content).
## Prerequisites
Before configuring MinIO:
1. **Cloud Storage Plugin** - Purchase from [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content)
2. **MinIO AIStor Installation** - Follow the [official installation guide](https://docs.min.io/enterprise/aistor-object-store/)
3. **Bucket** - Create a bucket for your files
4. **Access Keys** - Generate API credentials
5. **Network Access** - Ensure Aikeedo can reach MinIO
## Configuration Steps
Open the AIStor web console:
1. Navigate to your AIStor console URL
2. Login with your admin credentials
3. Ensure you have the necessary permissions for bucket and access key management
For detailed installation and setup instructions, refer to the [official AIStor installation guide](https://docs.min.io/enterprise/aistor-object-store/).
Create a bucket for your files:
1. In the AIStor console, go to **Buckets**
2. Click **Create Bucket**
3. Enter bucket name (e.g., `aikeedo-files`)
4. Choose access policy (Private recommended)
5. Click **Create Bucket**
For detailed bucket management, refer to the [official bucket management documentation](https://docs.min.io/enterprise/aistor-object-store/administration/console/managing-objects/).
Generate API credentials:
1. Go to **Access Keys** in the AIStor console
2. Click **Create Access Key**
3. Enter a name for the key (e.g., `aikeedo-access`)
4. Copy the **Access Key** and **Secret Key**
5. Save credentials securely
For detailed access management, refer to the [official IAM documentation](https://docs.min.io/enterprise/aistor-object-store/administration/iam/access/).
In your admin panel:
1. Go to **Settings** > **Cloud storage** > **MinIO**
2. Toggle **Status** to **Enabled**
3. Enter **Origin endpoint**: `http://your-server:port` (use your configured port)
4. Enter **Custom domain** (optional domain for MinIO)
5. Enter **Bucket name**: Your MinIO bucket name
6. Enter **Path prefix** (optional subfolder)
7. Enter **Access key**: Your MinIO access key
8. Enter **Secret key**: Your MinIO secret key
9. Click **Save changes**
## Configuration Fields
**Required fields:**
* **Origin endpoint** - MinIO server endpoint URL (e.g., `http://your-server:port`)
* **Bucket name** - Name of your MinIO bucket
* **Access key** - MinIO access key for authentication
* **Secret key** - MinIO secret key for authentication
**Optional fields:**
* **Custom domain** - Custom domain for serving files through reverse proxy
* **Path prefix** - Optional subfolder within your bucket for organizing files
For detailed server configuration options, refer to the [official core settings documentation](https://docs.min.io/enterprise/aistor-object-store/reference/aistor-server/settings/core/?tab=object-store-cli-options-configuration-setting).
## Troubleshooting
**Connection refused:**
* Check if MinIO server is running
* Verify the configured port is open and accessible
* Check firewall settings
* Ensure correct endpoint URL
**Access denied errors:**
* Verify access keys are correct
* Check bucket name matches exactly
* Ensure bucket exists and is accessible
* Verify network connectivity
**Files not uploading:**
* Check server resources (CPU, RAM, disk)
* Verify network latency between Aikeedo and MinIO
* Ensure data directory is accessible
* Check for disk errors
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Cloud Storage
Source: https://docs.aikeedo.com/integrations/cloud-storage/overview
Configure cloud storage providers for file uploads and AI-generated content in Aikeedo. Choose from AWS S3, Digital Ocean Spaces, Wasabi, Cloudflare R2, or MinIO.
## Overview
The Cloud Storage plugin extends Aikeedo with support for multiple cloud storage providers, enabling you to store user uploads and AI-generated files in the cloud instead of local storage. This improves performance, scalability, and reliability for your AI SaaS platform.
The Cloud Storage plugin is available for purchase from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content).
## Supported Providers
The Cloud Storage plugin supports the following providers:
Amazon Web Services Simple Storage Service - Industry standard with high durability and availability.
S3-compatible storage with simple pricing and easy setup.
S3-compatible storage with zero egress fees.
High-performance cloud storage with S3-compatible API.
Self-hosted object storage server for complete control.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Wasabi
Source: https://docs.aikeedo.com/integrations/cloud-storage/wasabi
Configure Wasabi cloud storage for file uploads and AI-generated content in Aikeedo. High-performance S3-compatible storage with competitive pricing.
## Overview
Wasabi is a high-performance cloud storage service that offers S3-compatible API with competitive pricing and no egress fees. It's designed for businesses that need reliable, fast storage without the complexity of traditional cloud providers.
**Key features:**
* **No egress fees** - Download as much as you want without additional charges
* **S3-compatible API** - Full AWS S3 API compatibility with additional features
* **Always consistent** - Immediate consistency for all operations (no eventual consistency)
* **11 9's durability** - Enterprise-grade reliability
* **Hot cloud storage** - Fast access to all data
* **Global availability** - Multiple regions worldwide
## Prerequisites
Before configuring Wasabi:
1. **Wasabi Account** - Create an account at [wasabi.com](https://wasabi.com)
2. **Bucket** - Create a bucket for your files
3. **Access Keys** - Generate API credentials
4. **Region** - Choose appropriate region
## Configuration Steps
In your Wasabi console:
1. On the Wasabi menu, click **Buckets**
2. In the upper right of the Console panel, click **Create Bucket**
3. Enter a unique DNS-compliant bucket name (e.g., `your-app-storage`)
4. Select the region where you want the bucket to reside
5. Click **Create Bucket** to create immediately, or click **Next** to configure additional properties
6. If continuing with setup, review bucket properties (versioning, logging, replication, tags) and click **Create Bucket**
Bucket names must be globally unique across all Wasabi accounts. Names must be 3-63 characters long, begin with a lowercase letter or number, and consist of lowercase letters, numbers, periods, and/or dashes. Cannot contain underscores, end with a dash, have consecutive periods, or use dashes adjacent to periods.
Generate API credentials:
1. Follow the steps in [Creating a User Account and Access Key](https://docs.wasabi.com/docs/creating-a-user-account-and-access-key#creating-a-user)
2. Create a new user account or use an existing one
3. Generate access keys for the user
4. Copy the **Access Key** and **Secret Key**
5. Save credentials securely
Access keys provide full access to your Wasabi account. Store them securely.
Set up CORS for your domain:
1. Go to your bucket settings
2. Click **Permissions** tab
3. Scroll to **CORS Configuration**
4. Click **Edit** button
5. In the JSON editor, configure the CORS policy:
```json theme={null}
[
{
"AllowedOrigins": [
"https://yourdomain.com",
"https://www.yourdomain.com"
],
"AllowedMethods": [
"GET"
],
"AllowedHeaders": [
"*"
],
"MaxAgeSeconds": 3000
}
]
```
6. Click **Save**
Wasabi uses JSON format for CORS configuration. Wasabi automatically provides CORS headers when an Origin header is present in requests, but you can also configure custom CORS policies for more specific control. For detailed CORS configuration, refer to the [official Wasabi CORS documentation](https://docs.wasabi.com/docs/cross-origin-resource-sharing-cors). Replace `yourdomain.com` with your actual domain.
The CORS policy uses JSON format in Wasabi. Make sure your JSON syntax is valid before saving.
In your admin panel:
1. Go to **Settings** > **Cloud storage** > **Wasabi**
2. Toggle **Status** to **Enabled**
3. Enter **Endpoint**: `https://s3..wasabisys.com` (replace `` with your actual region code)
4. Select **Region** from dropdown
5. Enter **Custom domain** (optional CDN domain)
6. Enter **Bucket name**: Your Wasabi bucket name
7. Enter **Path prefix** (optional subfolder)
8. Enter **Access key**: Your Wasabi access key
9. Enter **Secret key**: Your Wasabi secret key
10. Click **Save changes**
Enable Wasabi as your storage provider:
1. Go to **Settings** > **File storage**
2. In the **Adapter** dropdown, select **Wasabi**
3. Configure **Group files** and **Secure URLs** as needed
4. Click **Save changes**
## Configuration Fields
**Required fields:**
* **Endpoint** - Wasabi S3 endpoint URL (e.g., `https://s3.us-east-1.wasabisys.com`)
* **Region** - Wasabi region where your bucket is located
* **Bucket name** - Name of your Wasabi bucket
* **Access key** - Wasabi access key for authentication
* **Secret key** - Wasabi secret key for authentication
**Optional fields:**
* **Custom domain** - Custom domain for serving files through CDN
* **Path prefix** - Optional subfolder within your bucket for organizing files
## Troubleshooting
**Access denied errors:**
* Verify access keys are correct and active
* Check bucket name matches exactly
* Ensure bucket exists in the correct region
* Verify endpoint URL includes correct region
**CORS issues:**
* Check CORS configuration in bucket settings
* Verify allowed origins include your domain (with https\://)
* Ensure allowed methods include GET (sufficient for Aikeedo)
* Test with browser developer tools
**Files not uploading:**
* Verify bucket name and region are correct
* Check network connectivity to Wasabi
* Ensure bucket exists and is accessible
* Verify endpoint URL format is correct
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Cohere AI Integration
Source: https://docs.aikeedo.com/integrations/cohere
Enhance your Aikeedo platform with Cohere's advanced language AI models. This guide walks you through setting up and leveraging Cohere's powerful capabilities for chat and content creation.
## Introduction
Integrating Cohere AI into your Aikeedo platform unlocks a range of powerful language AI capabilities. Cohere's models excel in natural language understanding, generation, and task completion, making them ideal for enhancing your chat and writing tools.
## Setting Up the Integration
Follow these steps to connect Cohere AI with your Aikeedo platform:
### Step 1: Obtain Your Cohere API Key
1. If you don't have a Cohere account, [sign up here](https://cohere.ai/).
2. Once logged in, navigate to your Cohere dashboard.
3. Locate and copy your API key from the [API Keys page](https://dashboard.cohere.com/api-keys)—you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to Cohere AI services and should not be shared publicly.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > Cohere AI**.
3. Paste your Cohere API key into the designated "API Key" field.
4. Click **Save changes** to activate the integration.
Excellent! Cohere AI is now integrated with your Aikeedo platform.
## Aikeedo Tools Powered by Cohere AI
Cohere AI integration enhances the following Aikeedo tools:
1. **Chat**: Engage in intelligent, context-aware conversations with users.
2. **Writer**: Answer a template's questions and continue the result in chat.
3. **Utilities**: Generate titles and enhance prompts.
## Best Practices
To maximize your Cohere AI integration:
* Experiment with different models to find the best balance of performance and efficiency for your use case.
* Provide clear and specific prompts to get the most accurate and relevant outputs.
* Utilize Cohere's strength in understanding context by providing relevant background information in your prompts.
* Regularly review and update your usage to align with Cohere's evolving best practices and model capabilities.
## Troubleshooting
If you encounter issues with the Cohere AI integration:
1. Verify that your API key is correct and active.
2. Check your Cohere account for any usage limits or restrictions.
3. Ensure your Aikeedo platform is up to date.
4. If you receive error messages related to model access or quotas, verify your account status with Cohere.
Remember to review Cohere's usage policies and pricing to ensure compliance and manage costs effectively as you scale your Cohere AI integration.
# CurrencyAPI Integration
Source: https://docs.aikeedo.com/integrations/currency-api
Integrate CurrencyAPI for real-time currency exchange rates in Aikeedo to support multi-currency pricing and automatic conversion.
## Introduction
CurrencyAPI integration enables automatic currency conversion in your Aikeedo platform. This is essential when you accept payments in multiple currencies but your payment gateway only supports specific currencies, or when you want to display prices in users' local currencies.
## Prerequisites
Before you begin, ensure you have:
* An active [CurrencyAPI account](https://currencyapi.com/)
* Access to your Aikeedo admin panel
* Multi-currency payment requirements
## Why Use a Currency Rate Provider?
Using a currency rate provider like CurrencyAPI is **highly recommended** for several reasons:
1. **MRR Calculation**: Monthly Recurring Revenue (MRR) calculations depend on accurate exchange rates
2. **Analytics Accuracy**: Revenue reporting and analytics require proper currency conversion
3. **Multi-Currency Support**: Enable pricing and payments in multiple currencies
4. **Automatic Conversion**: Seamlessly convert between currencies during checkout
The default currency rate provider is set to "None" (null provider). Without a currency rate provider, features like MRR calculation will not work correctly, and multi-currency support will be limited.
## How CurrencyAPI Works in Aikeedo
CurrencyAPI provides real-time exchange rates that Aikeedo uses to:
1. **Automatic Currency Conversion**: Convert between currencies during checkout
2. **Multi-Currency Support**: Display prices in different currencies
3. **Payment Gateway Compatibility**: Convert to supported currencies when processing payments
4. **Accurate Financial Metrics**: Calculate MRR and other revenue metrics correctly
If your selected currency isn't directly supported by your payment gateway (Stripe or PayPal), Aikeedo automatically converts it to a supported currency using CurrencyAPI rates during checkout.
### Update Frequency
Aikeedo fetches currency rates from CurrencyAPI **once every 4 hours**, which means:
* **6 API requests per day**
* **\~180 API requests per month**
The free tier offers 300 requests per month, which is more than sufficient for Aikeedo's usage pattern. You don't need a paid plan for standard use!
## Step-by-Step Setup
1. Visit [CurrencyAPI.com](https://currencyapi.com/)
2. Click "Get Free API Key" or "Sign Up"
3. Complete the registration process
4. Verify your email address
CurrencyAPI offers a free tier with 300 requests per month, which is suitable for testing and small-scale deployments.
1. Log in to your CurrencyAPI dashboard
2. Navigate to your account dashboard
3. Locate your API key (it will be displayed prominently)
4. Copy the API key for use in Aikeedo
Keep your API key secure and confidential. Do not share it publicly or commit it to version control.
1. Log in to your Aikeedo admin panel
2. Navigate to **Settings > Billing**
3. Locate the "Currency Rate Provider" dropdown
4. Select **CurrencyAPI** from the options (default is "None")
5. Enter your CurrencyAPI API key in the "API Key" field
6. Click "Save changes" to activate the integration
The default currency rate provider is "None" (null provider). Changing it to CurrencyAPI enables proper MRR calculations and multi-currency support.
CurrencyAPI is now integrated with your Aikeedo platform!
1. Go to **Settings > General** (or Billing)
2. Set a currency that's not directly supported by your payment gateway
3. Create a test subscription or purchase
4. Verify that currency conversion happens correctly during checkout
Monitor your CurrencyAPI dashboard to see API requests and ensure the integration is working properly.
## Use Cases
### Multi-Currency Pricing
Display subscription prices in multiple currencies while accepting payments through a single payment gateway.
**Example:**
* Your payment gateway (Stripe) supports USD, EUR, GBP
* You want to offer pricing in JPY, AUD, CAD
* CurrencyAPI converts these currencies to USD during checkout
### Local Currency Display
Show prices in users' local currencies for better user experience, even if payments are processed in a different currency.
### Exchange Rate Management
Keep exchange rates up-to-date automatically without manual intervention, ensuring fair pricing across all currencies.
## Configuration Options
### Default Currency
Set your default currency in **Settings > Billing**:
```
Settings > Billing > Default Currency
```
This is the base currency for your platform. All other currencies will be converted relative to this currency.
### Supported Currencies
CurrencyAPI supports 150+ world currencies, including:
* USD (US Dollar)
* EUR (Euro)
* GBP (British Pound)
* JPY (Japanese Yen)
* AUD (Australian Dollar)
* CAD (Canadian Dollar)
* CHF (Swiss Franc)
* CNY (Chinese Yuan)
* And many more...
View the complete list at [CurrencyAPI Currencies](https://currencyapi.com/docs/currencies).
## API Rate Limits
CurrencyAPI offers different pricing tiers:
* **300 requests per month**
* **Perfect for Aikeedo**: With updates every 4 hours, Aikeedo uses only \~180 requests per month
* Suitable for production deployments
* No credit card required
The free tier is **sufficient for most Aikeedo installations**, including production environments!
* Higher request limits (5,000 to 100,000+ per month)
* More frequent update options
* Consider only if you need custom update frequencies
* [View pricing](https://currencyapi.com/pricing)
Since Aikeedo fetches rates every 4 hours (180 API calls/month), the free tier provides a comfortable buffer. You only need a paid tier if you modify the update frequency or have multiple Aikeedo installations using the same API key.
## How Aikeedo Uses Exchange Rates
1. **Update Schedule**: Rates are fetched from CurrencyAPI every 4 hours (6 times per day)
2. **Caching**: Exchange rates are cached between updates to minimize API requests
3. **MRR Calculations**: Cached rates are used for calculating Monthly Recurring Revenue and other financial metrics
4. **Checkout Conversion**: When a user checks out:
* Their selected currency is checked against payment gateway support
* If unsupported, CurrencyAPI rates convert it to a supported currency
* Payment is processed in the supported currency
5. **Accuracy**: Regular updates ensure fair and accurate conversions while staying within free tier limits
With updates every 4 hours, Aikeedo balances rate accuracy with API efficiency, using only \~180 requests per month.
## Troubleshooting
### Exchange Rates Not Updating
**Symptoms:**
* Prices showing outdated exchange rates
* Currency conversion seems incorrect
**Solution:**
1. Verify your CurrencyAPI key is correct
2. Check that you haven't exceeded your API rate limit
3. Clear the application cache from the admin panel Status page
4. Verify CurrencyAPI service status at [status.currencyapi.com](https://status.currencyapi.com/)
### Currency Conversion Not Working
**Symptoms:**
* Checkout fails with currency errors
* Prices not converting at checkout
**Solution:**
1. Ensure CurrencyAPI is selected as the Currency Rate Provider
2. Verify your API key is entered correctly
3. Check that the default currency is set in **Settings > Billing**
4. Ensure the currencies you're using are supported by CurrencyAPI
5. Review error logs for specific error messages
### API Key Invalid Error
**Symptoms:**
* Error message about invalid API key
* Currency conversion fails
**Solution:**
1. Double-check your API key in the CurrencyAPI dashboard
2. Ensure there are no extra spaces when pasting the key
3. Verify your CurrencyAPI account is active
4. Check if you've exceeded your API rate limit
## Best Practices
1. **Always Use a Currency Rate Provider**: Even for single-currency setups, enable CurrencyAPI for proper MRR calculations
2. **Start with Free Tier**: The free tier (300 requests/month) is sufficient for Aikeedo's usage (\~180 requests/month)
3. **Monitor API Usage**: Check your CurrencyAPI dashboard occasionally to ensure you're within limits
4. **Test Before Launch**: Test currency conversion with different currencies before going live
5. **Keep API Key Secure**: Store your API key securely and never commit it to version control
6. **Plan for Growth**: If you run multiple Aikeedo installations, consider a paid tier or use separate API keys
## Alternative: No Currency Rate Provider
The default setting is "None" (null provider), which means:
* All transactions use the default currency only
* No automatic conversion occurs
* Users must pay in the currency you specify
* **MRR calculations and revenue analytics will not work correctly**
* Multi-currency support is disabled
When set to "None," attempting to use currencies not supported by your payment gateway will result in checkout errors. Additionally, features that depend on currency conversion (like MRR calculation) will not function properly.
**Recommendation:** Even if you only accept one currency, setting up CurrencyAPI is highly recommended for accurate financial reporting and future flexibility. The free tier is sufficient and requires no ongoing costs.
## Related Guides
* [Billing Overview](/billing/overview) - Configure default currency and billing settings
* [Stripe Integration](/integrations/payment-gateways/stripe) - Set up Stripe payment gateway
* [PayPal Integration](/integrations/payment-gateways/paypal) - Set up PayPal payment gateway
* [Plans & Subscriptions](/billing/plans-snapshots-subscriptions) - Create pricing plans
## Additional Resources
* [CurrencyAPI Documentation](https://currencyapi.com/docs)
* [CurrencyAPI Pricing](https://currencyapi.com/pricing)
* [Supported Currencies List](https://currencyapi.com/docs/currencies)
* [CurrencyAPI Status Page](https://status.currencyapi.com/)
With CurrencyAPI integrated, your Aikeedo platform can now handle multi-currency transactions seamlessly!
# Custom Scripts Integration
Source: https://docs.aikeedo.com/integrations/custom-scripts
Enhance your Aikeedo platform with custom HTML, CSS, and JavaScript. This guide walks you through adding custom scripts and content to your Aikeedo website.
## Introduction
Aikeedo's Custom Scripts feature allows you to add custom HTML, CSS, and JavaScript to your platform. This powerful functionality enables you to extend the capabilities of your Aikeedo website, integrate additional tools, or customize the appearance and behavior of your site.
## Setting Up Custom Scripts
Follow these steps to add custom scripts to your Aikeedo platform:
1. Log in to your Aikeedo admin panel.
2. Navigate to Settings.
3. Find and click on "Script Tags" in the Integrations section.
4. Look for the "Custom tags" option.
## Configuration Options
In the Custom tags section, you'll see three main areas for adding custom content:
### Head Content
* This is where you can add custom `` tags, CSS links, or other content that belongs in the `` section of your HTML.
* Content added here will be injected right after the `` tag of your website.
### Body Content
* Use this area to add custom HTML, JavaScript, or other content that you want to appear in the main body of your website.
* Content added here will be injected right after the `` tag of your website.
### Footer Content
* This section is ideal for adding tracking scripts, analytics code, or other content that should appear at the end of your web pages.
* Content added here will be injected right before the closing `` tag of your website.
## Adding Custom Scripts
1. Decide which section (Head, Body, or Footer) is most appropriate for your custom content.
2. Enter your custom HTML, CSS, or JavaScript into the corresponding text area.
3. Click the "Save changes" button at the bottom of the page to apply your custom scripts.
Your custom scripts are now integrated with your Aikeedo platform!
## Best Practices
To make the most of the Custom Scripts feature:
* Test your custom scripts thoroughly before adding them to your live site.
* Use the appropriate section for each type of content (e.g., CSS in the Head, JavaScript in the Footer).
* Be cautious when adding third-party scripts, as they can affect your site's performance and security.
* Keep your custom scripts organized and commented for easy maintenance.
* Regularly review and update your custom scripts to ensure they're still necessary and functioning correctly.
Adding custom scripts can potentially break your website's functionality or compromise its security. Only add scripts from trusted sources and that you fully understand.
## Troubleshooting
If you experience issues after adding custom scripts:
1. Check your browser's console for any JavaScript errors.
2. Verify that your custom HTML is well-formed and doesn't conflict with existing page elements.
3. Ensure that any external resources (like CSS or JavaScript files) are accessible and loading correctly.
4. If your site's functionality is broken, try removing the custom scripts one by one to identify the problematic code.
5. Consider using browser developer tools to debug and test your custom scripts.
It's a good practice to keep a backup of your original configuration before making significant changes with custom scripts.
# ElevenLabs Integration
Source: https://docs.aikeedo.com/integrations/elevenlabs
Enhance your Aikeedo platform with ElevenLabs' advanced text-to-speech and voice AI capabilities. This guide walks you through setting up and leveraging ElevenLabs' powerful models for voice generation and audio processing.
## Introduction
Aikeedo's ElevenLabs integration unlocks a world of AI-powered voice capabilities for your platform. From natural-sounding text-to-speech to voice isolation, ElevenLabs' advanced models can enhance various aspects of your Aikeedo experience.
## Setting Up the Integration
Follow these simple steps to connect ElevenLabs with your Aikeedo platform:
### Step 1: Obtain Your ElevenLabs API Key
1. If you don't have an ElevenLabs account, [sign up here](https://elevenlabs.io/sign-up).
2. Once logged in, navigate to your ElevenLabs dashboard.
3. In the bottom left corner of the page, click on your profile name.
4. In the dropdown menu, select "API Keys" to access your API key management page.
5. On the API Keys page, you'll find your existing API key or options to create a new one.
6. If you don't already have an API key, click on "Create new API key".
7. Copy the generated API key—you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to ElevenLabs services and should not be shared publicly.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > ElevenLabs**.
3. Paste your ElevenLabs API key into the designated field.
4. Click **Save Changes** to activate the integration.
Congratulations! ElevenLabs is now integrated with your Aikeedo platform.
## Voice Synchronization
Aikeedo synchronizes the available voices from your ElevenLabs account every 4 hours. This means that any new voices added to your ElevenLabs account library may take up to 4 hours to appear in the Aikeedo app.
Only voices available through the ElevenLabs API can be used with Aikeedo. Community-created voices are not available through the API by default. To use community voices, you must first add them to your ElevenLabs account voice library. Once added, they will become available through the voice list API and subsequently in Aikeedo after the next synchronization.
## Aikeedo Tools Powered by ElevenLabs
ElevenLabs integration enhances the following Aikeedo tools:
1. **VoiceOver**: Transform text into natural-sounding speech with a wide range of voices and languages.
2. **Voice Isolator**: Separate and enhance voice audio from background noise.
## Best Practices
To make the most of your ElevenLabs integration:
* Experiment with different voices to find the best fit for your use case.
* Use clear and well-formatted text input for optimal text-to-speech results.
* Consider the language and accent requirements of your project when selecting voices.
* Monitor your API usage to manage costs effectively.
* Regularly check your ElevenLabs account for new voices and add desired ones to your library.
## Troubleshooting
If you encounter issues with the ElevenLabs integration, try these steps:
1. Verify that your API key is correct and active.
2. Check your ElevenLabs account for any usage limits or restrictions.
3. Ensure your Aikeedo platform is up to date.
4. Confirm you have sufficient credit balance in your ElevenLabs account.
5. For model-specific issues, verify that your account has access to the required models.
6. If you don't see newly added voices, remember that synchronization can take up to 4 hours.
7. If you receive error messages related to voice generation or processing, check the ElevenLabs status page for any ongoing service issues.
Regularly review your ElevenLabs usage, available models, and voice library to ensure smooth integration with Aikeedo.
Remember to review ElevenLabs' usage policies and pricing to ensure compliance and manage costs effectively.
# Fal AI Integration
Source: https://docs.aikeedo.com/integrations/falai
Enhance your Aikeedo platform with Fal AI's advanced image generation capabilities. This guide walks you through setting up and leveraging Fal AI's powerful models for creating unique and high-quality images.
## Introduction
Aikeedo's Fal AI integration unlocks advanced image generation capabilities for your platform. With access to Fal AI's cutting-edge models, you can significantly enhance your Imagine tool, allowing users to create unique and high-quality images from text descriptions.
## Setting Up the Integration
Follow these steps to connect Fal AI with your Aikeedo platform:
### Step 1: Obtain Your Fal AI API Key
1. If you don't have a Fal AI account, [sign up here](https://www.fal.ai/).
2. Once logged in, navigate to your Fal AI dashboard.
3. In the top navigation menu, click on "API Keys".
4. You'll see a list of your existing API keys. If you don't have any, or want to create a new one, click the "Add Key" button in the top right corner.
5. Copy the API key to your clipboard — you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to Fal AI services and should not be shared publicly. Fal AI masks part of the key in the dashboard for security reasons.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > FalAI**.
3. In the "API" section, enter your Fal AI API Secret in the "API Secret" field.
4. Click **Save changes** to activate the integration.
Great job! Fal AI is now integrated with your Aikeedo platform.
## Aikeedo Tools Powered by Fal AI
Fal AI integration enhances the following Aikeedo tool:
1. **Imagine**: Create unique and high-quality images from text descriptions using Fal AI's advanced image generation models.
## Best Practices
To make the most of your Fal AI integration:
* Experiment with different models to find the best fit for your use case.
* Provide clear and detailed text descriptions for optimal image generation results.
* Consider the style and complexity requirements of your project when selecting models.
* Monitor your API usage to manage costs effectively.
* Regularly update your integration to access the latest models and features.
## Troubleshooting
If you encounter issues with the Fal AI integration, try these steps:
1. Verify that your API Secret is correct and active.
2. Check your Fal AI account for any usage limits or restrictions.
3. Ensure your Aikeedo platform is up to date.
4. If you receive error messages related to model access or quotas, verify your account status with Fal AI.
5. For model-specific issues, confirm that your account has access to the required models.
6. If you experience slow response times, consider using a different model or optimizing your requests.
Regularly review your Fal AI usage and available model options to ensure smooth integration with Aikeedo.
Remember to review Fal AI's usage policies and pricing to ensure compliance and manage costs effectively as you scale your image generation integration.
# Google Cloud Platform Integration
Source: https://docs.aikeedo.com/integrations/gcp
Enhance your Aikeedo platform with Google Cloud Platform's advanced AI capabilities. This guide walks you through setting up and leveraging GCP's powerful text-to-speech services for high-quality voice generation.
## Introduction
Aikeedo's Google Cloud Platform integration unlocks advanced text-to-speech capabilities for your platform. With access to a wide range of high-quality voices, GCP's services can significantly enhance your VoiceOver tool and audio content creation.
## Setting Up the Integration
Follow these steps to connect Google Cloud Platform with your Aikeedo platform:
### Step 1: Obtain Your GCP Service Account Credentials
1. If you don't have a Google Cloud Platform account, [sign up here](https://cloud.google.com/). You'll need to provide some basic information and a valid payment method to create an account.
2. Once logged in, access the Google Cloud Console:
* Go to [console.cloud.google.com](https://console.cloud.google.com).
* If this is your first time, you'll be prompted to create a new project. Otherwise, you can create a new project or select an existing one from the top-left dropdown menu.
3. Enable the Text-to-Speech API for your project:
* In the Cloud Console, go to the "APIs & Services" page.
* Click on the "+ ENABLE APIS AND SERVICES" button at the top.
* Search for "Cloud Text-to-Speech API" and select it.
* Click the "ENABLE" button to activate the API for your project.
4. Create a service account and download the JSON key file:
* Navigate to the [IAM & Admin > Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts) page in the Cloud Console.
* Click the "+ CREATE SERVICE ACCOUNT" button at the top of the page.
* Enter a name for your service account (e.g., "aikeedo-tts-service"), and optionally add a description.
* Click "CREATE AND CONTINUE".
* In the "Grant this service account access to project" section, you can skip this step as it's not necessary for our use case.
* Click "CONTINUE" and then "DONE" to finish creating the service account.
* On the Service Accounts page, find the account you just created and click the three dots menu on the right.
* Select "Manage keys" from the dropdown.
* Click "ADD KEY" and choose "Create new key".
* Select "JSON" as the key type and click "CREATE".
* The JSON key file will be automatically downloaded to your computer. Keep this file secure, as it contains sensitive information.
The JSON key file you've downloaded contains credentials that grant access to your GCP project and its resources. Treat it like a password and never share it publicly or commit it to version control systems.
Keep your service account JSON key file secure and confidential. It grants access to your GCP services and should not be shared publicly.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > Google Cloud Platform Settings**.
3. In the "Service account" section, click "Choose file" and select the JSON key file you downloaded from GCP.
4. Click **Save changes** to activate the integration.
Great job! Google Cloud Platform is now integrated with your Aikeedo platform.
## Available Voice Types
Aikeedo leverages GCP's Text-to-Speech service, which offers a variety of voice types:
* Standard voices
* Premium voices
* Studio voices
These voice types offer different levels of quality and naturalness, allowing you to choose the best fit for your specific use case.
## Aikeedo Tools Powered by GCP
Google Cloud Platform integration enhances the following Aikeedo tool:
1. **VoiceOver**: Transform text into high-quality, natural-sounding speech using a wide range of voices and languages.
## Best Practices
To make the most of your GCP integration:
* Experiment with different voice types (Standard, Premium, Studio) to find the best fit for your use case.
* Consider the language and accent requirements of your project when selecting voices.
* Monitor your API usage to manage costs effectively.
## Troubleshooting
If you encounter issues with the GCP integration, try these steps:
1. Verify that your service account JSON key file is correct and active.
2. Check your GCP project for any usage limits or restrictions.
3. Ensure the Text-to-Speech API is enabled for your project.
4. Confirm you have sufficient credit balance in your GCP account.
5. For voice-specific issues, verify that your account has access to the required voice types.
6. If you receive error messages related to authentication or API calls, double-check your service account permissions.
Regularly review your GCP usage and available voice options to ensure smooth integration with Aikeedo.
Remember to review Google Cloud Platform's usage policies and pricing to ensure compliance and manage costs effectively as you scale your text-to-speech integration.
# Google Analytics
Source: https://docs.aikeedo.com/integrations/google-analytics
Track user behavior, page views, and engagement metrics in Aikeedo using Google Analytics 4 (GA4) measurement and reporting.
## Introduction
Google Analytics 4 integration enables tracking and analysis of user behavior on your Aikeedo platform, providing insights to optimize user experience and engagement.
## Setting Up the Integration
Follow these steps to connect Google Analytics with your Aikeedo platform:
### Step 1: Create a Google Analytics Account and Property
1. If you don't have a Google Analytics account, [sign up here](https://analytics.google.com/).
2. Once logged in, create a new property for your Aikeedo website.
3. Follow Google's prompts to set up your property and obtain your Measurement ID (it starts with "G-").
If you're new to Google Analytics, consider watching their introductory videos or reading their setup guide for beginners.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to Settings.
3. Find and click on "Script Tags" in the Integrations section.
4. Look for the "Google Analytics" integration option.
5. In the "Config" section, you'll see two important fields:
* **Status**: This is a toggle switch to enable or disable Google Analytics integration.
* **GA4 Measurement ID**: This is where you'll enter your Google Analytics 4 Measurement ID.
6. Toggle the "Status" switch to enable the integration.
7. Enter your Google Analytics 4 Measurement ID (it should start with "G-") in the "GA4 Measurement ID" field.
8. Click the "Save changes" button to apply your configuration.
Great job! Google Analytics is now integrated with your Aikeedo platform.
This integration is intended only for Google Analytics 4. For other versions, you may need to use custom script tags.
If you can't find your Measurement ID, click the "Click here" link below the ID field for additional guidance.
## What This Integration Does
* Tracks page views across your Aikeedo website
* Monitors basic user interactions and behavior
* Provides insights on user demographics and interests (if enabled in your Google Analytics settings)
* Helps you understand which pages are most visited
## Best Practices
To make the most of your Google Analytics integration:
* Regularly check your Google Analytics dashboard for insights on page views and user behavior.
* Use the data to inform decisions about content creation and website improvements.
* Consider setting up custom events or enhanced measurement in your Google Analytics account for more detailed tracking.
* Familiarize yourself with Google Analytics 4 features to maximize the value of the collected data.
* Respect user privacy and comply with data protection regulations like GDPR.
* If you need more advanced tracking (e.g., custom events, goal conversions), consider working with a developer to implement custom Google Analytics code.
This integration injects the default Google Analytics script. While it provides valuable basic insights, setting up advanced features like goal tracking may require additional custom implementation.
## Troubleshooting
If you're not seeing data in your Google Analytics account:
1. Verify that you've entered the correct Measurement ID in Aikeedo.
2. Allow 24-48 hours for data to start appearing in your Google Analytics dashboard.
3. Use Google's real-time reports to check if current traffic is being tracked.
4. Ensure you don't have any ad-blocking software that might interfere with Analytics.
5. Check if you need to accept terms or complete any additional setup steps in your Google Analytics account.
Remember that Google Analytics data is not real-time for most reports. There's usually a delay of several hours before data appears in your standard reports.
# Google Tag Manager Integration
Source: https://docs.aikeedo.com/integrations/google-tag-manager
Enhance your Aikeedo platform with Google Tag Manager to manage and deploy marketing tags on your website. This guide walks you through setting up Google Tag Manager for your Aikeedo website.
## Introduction
Aikeedo's Google Tag Manager integration allows you to easily manage and deploy various marketing and analytics tags on your platform without modifying the code. By adding Google Tag Manager, you can efficiently implement tracking codes, conversion pixels, and other third-party scripts, providing flexibility in managing your website's tags.
## Setting Up the Integration
Follow these steps to connect Google Tag Manager with your Aikeedo platform:
### Step 1: Create a Google Tag Manager Account and Container
1. If you don't have a Google Tag Manager account, [sign up here](https://tagmanager.google.com/).
2. Once logged in, create a new container for your Aikeedo website.
3. Follow Google's prompts to set up your container and obtain your Container ID (it starts with "GTM-").
If you're new to Google Tag Manager, consider exploring their documentation or tutorials to understand how to set up tags, triggers, and variables.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to Settings.
3. Find and click on "Script Tags" in the Integrations section.
4. Look for the "Google Tag Manager" integration option.
5. In the "Config" section, you'll see two important fields:
* **Status**: This is a toggle switch to enable or disable Google Tag Manager integration.
* **GTM Container ID**: This is where you'll enter your Google Tag Manager Container ID.
6. Toggle the "Status" switch to enable the integration.
7. Enter your Google Tag Manager Container ID (it should start with "GTM-") in the "GTM Container ID" field.
8. Click the "Save changes" button to apply your configuration.
Great job! Google Tag Manager is now integrated with your Aikeedo platform.
## What This Integration Does
* Allows you to manage various marketing and analytics tags from a single interface
* Enables easy implementation of tracking codes, conversion pixels, and other scripts
* Provides flexibility to add, edit, or remove tags without directly modifying your website's code
* Facilitates A/B testing and personalization through tag management
## Best Practices
To make the most of your Google Tag Manager integration:
* Use Google Tag Manager to implement your Google Analytics tracking (if you're using it)
* Create a clear naming convention for your tags, triggers, and variables
* Use built-in templates when possible to reduce errors
* Test your tags thoroughly in a non-production environment before publishing
* Use workspaces and versions to manage changes and rollbacks
* Regularly audit your tags to remove any that are no longer needed
* Consider setting up user permissions to control who can make changes to your tags
While Google Tag Manager makes it easier to manage tags, it's important to understand the impact of each tag on your website's performance and user privacy.
## Troubleshooting
If you're experiencing issues with your Google Tag Manager integration:
1. Verify that you've entered the correct Container ID in Aikeedo.
2. Use Google Tag Manager's Preview mode to check if the container is loading correctly on your website.
3. Check the browser console for any JavaScript errors that might be preventing tags from firing.
4. Ensure that there are no conflicts between tags implemented via Google Tag Manager and any hardcoded scripts on your website.
5. Verify that your tags are set up correctly in the Google Tag Manager interface, with appropriate triggers and variables.
Google Tag Manager's Debug mode is a powerful tool for troubleshooting tag issues. Use it to see which tags are firing and why.
# Intercom Integration
Source: https://docs.aikeedo.com/integrations/intercom
Enhance your Aikeedo platform with Intercom to provide real-time customer support and engagement. This guide walks you through setting up Intercom for your Aikeedo website.
## Introduction
Aikeedo's Intercom integration allows you to add powerful customer messaging and support capabilities to your platform. By integrating Intercom, you can engage with your users in real-time, provide support, and gather valuable insights about your customer interactions.
## Setting Up the Integration
Follow these steps to connect Intercom with your Aikeedo platform:
### Step 1: Create an Intercom Account and Obtain Necessary Credentials
1. If you don't have an Intercom account, [sign up here](https://www.intercom.com/).
2. Once logged in, navigate to the Settings menu in your Intercom dashboard.
3. In the left sidebar, click on "Messenger" under the "Channels" section.
4. On the Messenger settings page, click on the "Install" tab.
5. You'll see two important pieces of information here:
* **Workspace/App ID**: This is displayed at the top of the installation code snippet. It's a string that looks like "abc123de".
* **Secret Key**: Click on "Copy secret key" to copy your secret key. It's a long string of characters used for identity verification.
The Secret Key is crucial for enabling Identity Verification, which adds an extra layer of security to your integration. Never share this key publicly or commit it to version control systems.
Intercom recommends using Identity Verification to ensure the security and integrity of your user data. Make sure to enable this feature in both Intercom and Aikeedo settings.
6. Copy both the Workspace/App ID and the Secret Key—you'll need these for configuring Aikeedo.
Keep your Secret Key confidential. As the Intercom dashboard warns: "Never commit it directly to your repository, client-side code, or anywhere a third party can find it."
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to Settings.
3. Find and click on "Script Tags" in the Integrations section.
4. Look for the "Intercom" integration option.
5. In the "Config" section, you'll see the following fields:
* **Status**: A toggle switch to enable or disable the Intercom integration.
* **Identity verification**: A toggle to enable or disable identity verification.
* **Workspace/App ID**: Enter your Intercom Workspace/App ID here.
* **Secret key**: Enter your Intercom Secret Key here (required for identity verification).
6. Toggle the "Status" switch to enable the integration.
7. We recommend enabling "Identity verification" for enhanced security.
8. Enter your Intercom Workspace/App ID in the designated field.
9. If you've enabled identity verification, enter your Intercom Secret Key.
10. Click the "Save changes" button to apply your configuration.
Great job! Intercom is now integrated with your Aikeedo platform.
The Secret Key is required to enable Identity Verification, which adds an extra layer of security to your integration.
If you can't find your Workspace/App ID, click the "Click here" link below the ID field for additional guidance.
## What This Integration Does
* Adds the Intercom chat widget to your Aikeedo website
* Allows real-time communication with your users
* Enables you to provide customer support directly through your website
* Helps you gather user feedback and insights
* Facilitates user onboarding and engagement through targeted messages
## Best Practices
To make the most of your Intercom integration:
* Set up welcome messages to greet new users and guide them through your platform
* Use Intercom's targeted messaging features to engage users based on their behavior
* Customize the appearance of the Intercom widget to match your brand
* Set up an organized inbox and team assignments for efficient customer support
* Utilize Intercom's knowledge base feature to create self-service support resources
* Regularly review conversation data and user feedback to improve your product and support
Remember to respect user privacy and comply with data protection regulations when collecting and handling user data through Intercom.
## Troubleshooting
If you're experiencing issues with your Intercom integration:
1. Verify that you've entered the correct Workspace/App ID in Aikeedo.
2. If using identity verification, ensure your Secret Key is correct and up to date.
3. Check if the Intercom widget appears on your website after enabling the integration.
4. Clear your browser cache and cookies, then reload your website to see if the widget appears.
5. Ensure there are no JavaScript errors in your browser console that might be preventing Intercom from loading.
6. If the widget appears but isn't functioning correctly, verify your Intercom account settings and workspace configuration.
Intercom provides a test mode in their dashboard. Use this to verify your integration without affecting real users.
# Ollama Integration
Source: https://docs.aikeedo.com/integrations/ollama
Run open-source large language models locally with Ollama integration. This guide explains how to set up and configure Ollama for your Aikeedo platform.
## Introduction
Ollama allows you to run large language models locally on your own hardware. By integrating Ollama with Aikeedo, you can provide AI capabilities without relying on cloud services, ensuring data privacy and reducing costs.
Currently, Ollama integration is available only for the Chat tool in Aikeedo.
## About Ollama
Ollama is an open-source project that simplifies running and managing large language models locally. It offers:
* Easy installation and setup
* Support for various open-source models
* Local execution for enhanced privacy
* Custom model configuration
* REST API for integration
For more information, visit the [official Ollama website](https://ollama.ai).
## Setting Up Ollama
### Step 1: Install Ollama
```bash theme={null}
curl -fsSL https://ollama.ai/install.sh | sh
```
For Apple Silicon (M1/M2) Macs, Metal GPU acceleration is supported automatically.
```bash theme={null}
curl -fsSL https://ollama.ai/install.sh | sh
```
For NVIDIA GPUs:
* Ensure CUDA 11.7 or later is installed
* Install NVIDIA container toolkit
For AMD GPUs:
* ROCm 5.6 or later is required
* Check hardware compatibility in ROCm documentation
1. Download the latest installer from [Ollama Windows Release](https://github.com/ollama/ollama/releases)
2. Run the downloaded installer
3. Follow the installation wizard
Requirements:
* Windows 10 or later
* For NVIDIA GPUs, install CUDA 11.7 or later
For detailed installation instructions and troubleshooting, refer to the [Ollama Installation Guide](https://github.com/ollama/ollama#installation).
### Step 2: Configure Your Model
1. Start the Ollama service
2. Pull your desired model. For example:
```bash theme={null}
ollama pull llama2
# or
ollama pull mistral
# or
ollama pull llama2-uncensored
```
View all available models at [Ollama Model Library](https://ollama.ai/library).
### Step 3: Configure Server Address
By default, Ollama runs on:
```
http://localhost:11434
```
For remote access, you'll need to:
1. Configure your firewall to allow access to port 11434
2. Set up proper security measures as Ollama doesn't include authentication by default
When exposing Ollama to remote access, ensure you implement appropriate security measures to protect your server.
## Integrating with Aikeedo
### Step 1: Configure Aikeedo
1. Log in to your Aikeedo admin panel
2. Navigate to Settings → Integrations → Ollama
3. Enter your Ollama server address (e.g., `http://localhost:11434`)
4. Add your models:
* **Key**: A unique identifier (e.g., `ollama/llama2:latest`)
* **Name**: Display name for the model
* **Provider**: Set as "Meta" for Llama models, or appropriate provider
5. Click "Save changes"
The server address must include the scheme (http/https), host, and port number.
## Model Configuration
You can customize model behavior using Modelfiles. Example:
```text theme={null}
FROM llama2
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER top_k 40
```
Learn more about model configuration in the [Ollama Documentation](https://github.com/ollama/ollama/blob/main/docs/modelfile.md).
## Hardware Requirements
Minimum requirements vary by model:
* 7B models: 8GB RAM
* 13B models: 16GB RAM
* 33B+ models: 32GB+ RAM
GPU acceleration is supported for:
* NVIDIA GPUs with CUDA
* AMD GPUs with ROCm
* Apple Silicon (Metal)
## Best Practices
1. **Model Selection**:
* Start with smaller models (7B) and test performance
* Consider your hardware capabilities
* Choose models based on your specific use case
2. **Performance**:
* Enable GPU acceleration when available
* Monitor system resources
* Adjust model parameters for optimal performance
3. **Security**:
* Implement proper firewall rules
* Use reverse proxy for additional security
* Regular system updates
## Troubleshooting
Common issues and solutions:
1. **Connection Issues**:
* Verify Ollama service is running
* Check server address format
* Confirm firewall settings
2. **Model Loading Failures**:
* Ensure sufficient system resources
* Verify model is properly pulled
* Check model compatibility
3. **Performance Issues**:
* Monitor system resources
* Consider using a smaller model
* Adjust model parameters
## Additional Resources
* [Ollama GitHub Repository](https://github.com/ollama/ollama)
* [Ollama Documentation](https://github.com/ollama/ollama/tree/main/docs)
* [Ollama API Reference](https://github.com/ollama/ollama/blob/main/docs/api.md)
* [Ollama Discord Community](https://discord.gg/ollama)
Keep Ollama and your models updated for the best performance and security.
# OneSignal Integration
Source: https://docs.aikeedo.com/integrations/onesignal
Enhance your Aikeedo platform with OneSignal's push notification capabilities. This guide walks you through setting up and leveraging OneSignal for engaging your users with timely notifications.
## Introduction
Aikeedo's OneSignal integration enables powerful push notification functionality for your platform. By integrating OneSignal, you can significantly enhance user engagement by sending timely and relevant notifications to your users across various devices.
## Setting Up the Integration
Follow these steps to connect OneSignal with your Aikeedo platform:
### Step 1: Create a OneSignal App and Obtain Your App ID
1. If you don't have a OneSignal account, [sign up here](https://onesignal.com/).
2. Once logged in, click on "All Apps" in the left sidebar.
3. Click the "New App/Website" button to create a new app.
4. Fill in the following details:
* **OneSignal App Name**: Enter a name for your Aikeedo integration (e.g., "Aikeedo Notifications").
* **Select your organization**: Choose the appropriate organization from the dropdown.
5. Under "Set up your first channel", select "Web" as we're integrating with a web application.
6. Click "Next: Configure Your Platform" to proceed.
7. On the Web Configuration page, choose "Custom Code" as the integration type.
8. Follow the prompts to complete the web push setup for your domain using the Custom Code integration.
9. Once setup is complete, navigate to "Settings" > "Keys & IDs" in the left sidebar.
10. Under the "OneSignal Keys" section, you'll find your "OneSignal App ID". It's a long string of characters (e.g., 900877c5-995b-49ef-95e8-af7d52f36054).
11. Copy this App ID—you'll need it for Aikeedo.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > OneSignal**.
3. In the "Configuration" section, you'll see the following options:
* **Status**: Toggle this to enable or disable the OneSignal integration.
* **App ID**: Enter your OneSignal App ID in this field.
4. Paste your OneSignal App ID into the "App ID" field.
5. Enable the integration by toggling the "Status" switch to the enabled position.
6. Click **Save changes** to activate the integration.
Great job! OneSignal is now integrated with your Aikeedo platform.
## Best Practices
To make the most of your OneSignal integration:
* Craft clear and concise notification messages to engage users effectively.
* Use segmentation to send targeted notifications to specific user groups.
* Test your notifications across different devices and platforms to ensure consistency.
* Monitor notification performance and user engagement metrics in your OneSignal dashboard.
## Troubleshooting
If you encounter issues with the OneSignal integration, try these steps:
1. Verify that your App ID is correct and active.
2. Ensure that webpush is properly set up in your OneSignal account with Custom Code.
3. Check that the integration is enabled in your Aikeedo settings.
4. Test the integration by sending a test notification from your OneSignal dashboard.
5. If you experience delivery issues, check your OneSignal dashboard for any error messages or delivery reports.
6. Ensure your Aikeedo platform is up to date.
Can't find your App ID? Click the "Click here" link below the App ID field in the Aikeedo settings for additional guidance.
Remember to review OneSignal's usage policies and pricing to ensure compliance and manage costs effectively as you scale your push notification integration.
# OpenAI Integration
Source: https://docs.aikeedo.com/integrations/openai
Integrate OpenAI models into Aikeedo for chat, content generation, image creation, transcription, and text-to-speech capabilities.
## Introduction
The OpenAI integration enables access to latest models for chat, content generation, image creation, transcription, and text-to-speech capabilities within your Aikeedo platform.
## Setting Up the Integration
Follow these simple steps to connect OpenAI with your Aikeedo platform:
### Step 1: Obtain Your OpenAI API Key
1. If you don't have an OpenAI account, [sign up here](https://platform.openai.com/signup).
2. Once logged in, navigate to the [API keys page](https://platform.openai.com/account/api-keys).
3. Click on **Create new secret key**.
4. Copy the generated API key—you'll need this for Aikeedo.
Keep your API key secure. It provides access to OpenAI's services and should
not be shared publicly.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > OpenAI**.
3. Paste your OpenAI API key into the **API Secret** field.
4. Configure webhook settings (see Step 3 below).
5. Click **Save Changes** to activate the integration.
Congratulations! OpenAI is now integrated with your Aikeedo platform.
### Step 3: Configure Webhooks (Required)
Webhook configuration is required for OpenAI integration to function properly.
To enable real-time event notifications from OpenAI, you need to configure webhooks:
1. **Set up Webhook URL**: The webhook URL is automatically provided as `https://your-domain.com/webhooks/openai`
2. **Configure OpenAI Dashboard**:
* Go to your [OpenAI Webhooks Settings](https://platform.openai.com/settings/project/webhooks)
* Create a new webhook endpoint using the provided webhook URL
3. **Set Webhook Secret**:
* Generate a webhook secret in your OpenAI dashboard
* Copy the secret and paste it into the **Webhook secret** field in Aikeedo
4. **Select Events**: Ensure the following events are selected in your OpenAI webhook configuration:
* `video.completed`
* `video.failed`
Make sure to select all listed events in your OpenAI webhook configuration for
proper functionality.
## Custom API Keys (BYOK)
Aikeedo supports Bring Your Own Key (BYOK) functionality for OpenAI integration:
* **Workspace-level API keys**: Workspace owners can provide their own OpenAI API key
* **System override**: When configured, the workspace-specific API key takes precedence over the system-wide API key
* **Credit management**: Custom API keys bypass workspace credit deductions for OpenAI services
* **Subscription requirement**: A valid subscription plan and credit balance are still required for other features and API services
**BYOK is not available for features that require OpenAI webhooks.** Video
generation always uses the system-wide API key because it depends on webhook
notifications for proper functionality.
BYOK is particularly useful for organizations that want to manage their own
OpenAI billing and usage directly. For detailed information about BYOK
configuration and setup, see the [BYOK documentation](/advanced/byok).
## Aikeedo Tools Powered by OpenAI
OpenAI integration enhances the following Aikeedo tools:
1. **Chat**: Engage in intelligent conversations, with Canvas documents and clarifying questions.
2. **Writer**: Answer a template's questions and continue the result in chat.
3. **Imagine**: Create and edit images from text descriptions and reference images.
4. **Transcriber**: Convert audio to text with high accuracy.
5. **Voiceover**: Transform text into natural-sounding speech.
6. **Classifier**: Categorize and analyze text content.
7. **Utilities**: Generate titles and enhance prompts.
## Best Practices
To make the most of your OpenAI integration:
* Experiment with different models to find the best fit for your use case.
* Use clear and specific prompts to get the most accurate results.
* Monitor your API usage to manage costs effectively.
## Troubleshooting
If you encounter issues with the OpenAI integration, try these steps:
1. **API Key Issues**:
* Verify that your API key is correct and active
* Check your OpenAI account for any usage limits or restrictions
* Confirm you have sufficient credit balance in your OpenAI platform account
2. **Webhook Configuration**:
* Ensure webhook URL is correctly configured in your OpenAI dashboard
* Verify that the webhook secret matches between OpenAI and Aikeedo
* Check that all required events (`video.completed`, `video.failed`) are selected
3. **General Issues**:
* Ensure your Aikeedo platform is up to date
* Check network connectivity and firewall settings
* Verify SSL certificates are valid for webhook endpoints
4. **Model Access Issues**:
* For model-specific issues, verify that your account has access to the required models
* If you receive an error message like "The model \[model-name] does not exist or you do not have access to it":
* This error comes directly from OpenAI and indicates that the specific model is not enabled for your account
* Some models may require specific account tiers or permissions
* Check which models are accessible to you on the [OpenAI Playground](https://platform.openai.com/playground)
* Ensure your OpenAI account has sufficient credits and the correct billing setup for the models you're trying to use
5. **BYOK Issues**:
* If using custom API keys, ensure the workspace-specific key is valid and active
* Verify that the custom key has the necessary permissions for required models
* For comprehensive BYOK troubleshooting and configuration details, refer to the [BYOK documentation](/advanced/byok)
Regularly review your OpenAI usage and available models to ensure smooth
integration with Aikeedo.
Remember to review OpenAI's usage policies and pricing to ensure compliance
and manage costs effectively.
# OpenAI API-Compatible Servers
Source: https://docs.aikeedo.com/integrations/openai-api-compatibility
Learn how to integrate any OpenAI-compatible API provider with your Aikeedo platform.
## Introduction
Aikeedo supports any API provider that implements the OpenAI API specification. This means you can integrate any service that follows OpenAI's API format, whether it's a cloud service, self-hosted model, or custom implementation.
If a service is compatible with OpenAI's API specification, it will work with Aikeedo - there are no restrictions on which providers you can use.
## Common Providers
While you can use any OpenAI-compatible provider, here are some popular options:
* Together AI
* OpenRouter
* Groq
* DeepSeek
* Google AI (Gemini)
* Perplexity AI
* Hugging Face
* Nebius
* DeepInfra
* AI/ML API
* Azure OpenAI Service
* v0 by Vercel
* Self-hosted models (via compatible servers)
* Custom LLM implementations
Most of the providers below ship as ready-made servers with their current
models. Import them from **Settings → Starter content → LLM servers**, then add
your API key. See [Starter content](/advanced/seed-data#llm-servers-and-their-models).
## Adding a New Provider
1. Go to Settings → Integrations
2. Scroll to "Custom LLM Servers" section
3. Click "New server"
### Configuration Fields
#### API Server
* **Name**: A descriptive name for the provider
* **Server address**: The API endpoint URL
* **API Key/Authorization token**: Your authentication token for the service
The server address supports dynamic variables for flexible configuration.
##### **Dynamic Variables**
You can use the following variable in your server address:
* `{model}`: Will be dynamically replaced with the current model name
**Example for Azure OpenAI Service**:
```text theme={null}
https://my-instance.openai.azure.com/openai/deployments/{model}
```
When using model "gpt-4o-mini", the actual request will go to:
```text theme={null}
https://my-instance.openai.azure.com/openai/deployments/gpt-4o-mini
```
Dynamic variables are useful for services where the model name is part of the API endpoint. Most providers take the model name in the request body instead and don't need them.
#### Headers
Aikeedo automatically adds these default headers:
* `Content-Type: application/json`
* `Authorization: Bearer YOUR_API_KEY` (when API key field is filled)
You can override these default headers or add custom headers as needed. This is useful when a provider requires specific header configurations.
##### Adding Custom Headers
1. Click "Add header"
2. Enter the header key (e.g., `HTTP-Referer`)
3. Enter the header value
4. Repeat for additional headers
##### Examples
**Override default Authorization header**:
```text theme={null}
Key: Authorization
Value: Basic YOUR_BASE64_CREDENTIALS
```
**Add provider-specific headers**:
```text theme={null}
Key: X-Title
Value: Your Application Name
```
If you need to override the default Content-Type or Authorization headers, simply add them with your desired values in the custom headers section.
#### Models Configuration
For each model you want to use:
* **Key**: Unique identifier for the model (e.g., `gpt-3.5-turbo`)
* **Name**: Display name shown to users
* **Provider**: The model provider's name
* **Vision**: Toggle if the model supports image analysis
* **Tools**: Toggle if the model supports function calling
## Provider-Specific Setup
```text Server theme={null}
https://api.together.xyz/v1
```
Get your API key from [Together AI Settings](https://api.together.ai/settings/api-keys)
```text Server theme={null}
https://openrouter.ai/api/v1
```
Get your API key from [OpenRouter Settings](https://openrouter.ai/settings/keys)
```text Server theme={null}
https://generativelanguage.googleapis.com/v1beta/openai
```
Get your API key from [Google AI Studio](https://aistudio.google.com/apikey)
```text Server theme={null}
https://router.huggingface.co/v1
```
Get your API key from [Hugging Face Settings](https://huggingface.co/settings/tokens)
Use the model's Hugging Face ID as the model key, for example `deepseek-ai/DeepSeek-V4-Pro-0813`. Hugging Face routes the request to one of its inference providers.
The old `https://api-inference.huggingface.co/models/{model}/v1/` address no longer works. If you added Hugging Face with it, change the server address to the one above.
```text Server theme={null}
https://api.groq.com/openai/v1
```
Get your API key from [Groq Console](https://console.groq.com/keys)
Groq is known for its extremely fast inference speeds and competitive pricing.
```text Server theme={null}
https://api.deepseek.com
```
Required headers:
* `Authorization`: Bearer YOUR\_API\_KEY
Get your API key from [DeepSeek Platform](https://platform.deepseek.com/api_keys)
DeepSeek offers both general-purpose and code-specialized models with competitive pricing.
```text Server theme={null}
https://api.perplexity.ai
```
Get your API key from [Perplexity Settings](https://www.perplexity.ai/settings/api)
Perplexity offers high-quality models with strong reasoning capabilities and up-to-date knowledge.
```text Server theme={null}
https://api.tokenfactory.nebius.com/v1
```
Get your API key from [Nebius Token Factory](https://tokenfactory.nebius.com/)
Nebius AI Studio is now Nebius Token Factory. Servers added with the old `https://api.studio.nebius.ai/v1/` address still respond, but switch to the address above.
```text Server theme={null}
https://api.deepinfra.com/v1/openai/
```
Get your API key from [DeepInfra Dashboard](https://deepinfra.com/dash/api_keys)
DeepInfra provides access to a variety of open-source models with competitive pricing and low latency.
```text Server theme={null}
https://api.aimlapi.com/v1/
```
Get your API key from [AI/ML API Keys](https://aimlapi.com/app/keys)
```text Server theme={null}
https://:resource_name.openai.azure.com/openai/deployments/:deployment_name/chat/completions?api-version=:api_version
```
Get your API key and other details from [Azure Portal](https://portal.azure.com/#blade/Microsoft_Azure_ProjectOxford/CognitiveServicesHub/OpenAI)
For Azure OpenAI Service:
* Replace `:resource_name` with your Azure OpenAI resource name
* Replace `:deployment_name` with `{model}` to use your configured model names as deployment names
* Replace `:api_version` with your desired API version (e.g., '2024-10-21')
* Different endpoints (like completions, embeddings) will need their specific paths
Example configuration:
```text theme={null}
https://my-instance.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2024-10-21
```
When you configure a model named "gpt-4o-mini", it will automatically use that as the deployment name in the URL.
```text Server theme={null}
https://api.v0.dev/v1
```
Model name: `v0-1.0-md`
Get your API key from [v0.dev Settings](https://v0.dev/chat/settings/keys)
The v0 API is currently in beta and requires a Premium or Team plan with usage-based billing enabled.
## Tools Compatibility
When configuring models for your custom LLM server, you can enable them for:
1. **Chat**: Interactive conversations and assistance
2. **Writer**: Template-driven writing that continues in chat
3. **Title Generation**: Automatic title creation for content
4. **Prompt Enhancement**: Rewriting and generating prompts in the Imagine and Video composers
Make sure to enable only the capabilities that your chosen model actually supports. Enabling unsupported features may result in unexpected behavior.
## Best Practices
1. **Testing**:
* Test each model after configuration
* Verify response formats
* Check token limits and pricing
2. **Security**:
* Keep API keys secure
* Use HTTPS for external providers
* Regularly rotate API keys
3. **Monitoring**:
* Track API usage
* Monitor response times
* Check for error rates
## Troubleshooting
Common issues and solutions:
1. **Authentication Errors**:
* Verify API key format
* Check header configuration
* Confirm server address is correct
2. **Model Issues**:
* Ensure model names match provider's specifications
* Verify model availability in your subscription
* Check provider's status page
3. **Connection Problems**:
* Verify network connectivity
* Check for firewall restrictions
* Confirm server address format
Always refer to your provider's documentation for the most up-to-date configuration details and troubleshooting guides.
## Rate Limits and Quotas
* Monitor your provider's rate limits
* Check quota usage regularly
* Set up alerts for quota thresholds
* Consider implementing retry logic for rate limit errors
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
Keep your integrations updated as providers may change their API specifications or requirements.
# Bank Transfer Payment
Source: https://docs.aikeedo.com/integrations/payment-gateways/bank-transfer
Configure bank transfer payments in Aikeedo with domestic and international transfer options, beneficiary details, and bank information for secure offline payment processing.
Bank Transfer is a built-in payment method in Aikeedo that allows customers to pay via direct bank transfers. This payment option supports both domestic and international transfers with comprehensive bank details configuration.
## Overview
Bank Transfer payments provide an alternative to online payment gateways, allowing customers to transfer funds directly to your business bank account. This method is particularly useful for:
* High-value transactions
* B2B payments
* Customers who prefer traditional banking methods
* Regions with limited online payment gateway support
Bank Transfer is a built-in payment method and doesn't require additional plugins or external service setup.
## Prerequisites
Before configuring bank transfer payments, ensure you have:
* A business bank account with domestic and/or international transfer capabilities
* Complete bank details including routing numbers, SWIFT codes, and addresses
* Beneficiary information for your business
* Bank-issued documents (if required by your bank)
## Configuration
### Accessing Bank Transfer Settings
1. Log in to your Aikeedo admin panel
2. Navigate to **Settings > Payments > Bank Transfer**
3. You'll see three main configuration sections:
* **Details** (General settings)
* **Domestic Transfer** (Local bank transfers)
* **International Transfer** (Cross-border transfers)
### General Settings (Details)
Configure the basic bank transfer settings:
Toggle the **Status** switch to "Enabled" to activate bank transfer payments.
Set the **Enabled for** dropdown to specify which payment types support bank transfers:
* One time payments (Recommended)
* One-time and annual payments
* All payments
Choose **One time payments (Recommended)** from the dropdown for standard bank transfer processing.
Since bank transfers require manual verification, it's recommended to enable them only for one-time payments to avoid complications with recurring payment processing.
### Domestic Transfer Configuration
Set up domestic bank transfer details for local payments:
Toggle the **Status** switch to "Enabled" for domestic transfers.
Enter your business information:
* **Beneficiary name / Account holder**: Your business legal name
* **Account number**: Your business bank account number
* **Beneficiary address**: Complete business address
Provide your bank's information:
* **Bank name**: Your bank's official name
* **Routing number**: Bank routing number (US) or sort code (UK)
* **Bank address**: Complete bank branch address
* **Currency**: Select the primary currency (e.g., US Dollar)
* **Tax ID**: Your business tax identification number (optional)
* **Reference / Memo**: Payment reference information (optional)
If required by your bank, upload supporting documents:
* Click **Browse files** to upload bank-issued documents
* Use **View current** to review uploaded files
* Use **Delete** to remove outdated documents
### International Transfer Configuration
Configure international bank transfer details for cross-border payments:
Toggle the **Status** switch to "Enabled" for international transfers.
Enter international transfer information:
* **Beneficiary name / Account holder**: Your business legal name
* **IBAN / Account number**: International Bank Account Number
* **Beneficiary address**: Complete business address including country
Provide international banking information:
* **Bank name**: Your bank's official name
* **SWIFT / BIC Code**: Bank Identifier Code for international transfers
* **Bank address**: Complete bank address including country
* **Currency**: Select the currency for international transfers
* **Tax ID**: Your business tax identification number (optional)
* **Reference / Memo**: Payment reference with bank details
If your bank requires an intermediary bank:
* **Bank name**: Corresponding bank name
* **SWIFT / BIC Code**: Corresponding bank's SWIFT code
* **Account number**: Your account number with the corresponding bank
* **Bank address**: Corresponding bank's address
* Upload any required bank requisites documents
## Payment Processing Flow
### For Customers
When customers choose bank transfer payment:
1. **Payment Selection**: Customer selects "Bank Transfer" from available payment methods during checkout
2. **Order Placement**: Customer clicks "Place order" to complete the purchase
3. **Payment Instructions**: Customer is redirected to order details page with comprehensive bank transfer instructions
4. **Transfer Execution**: Customer initiates transfer through their bank using provided details
5. **Order Tracking**: Customer can monitor order status in their billing overview
6. **Order Fulfillment**: Order is processed after payment verification by admin
Payment details are provided after placing the order, not during checkout. This ensures customers have complete banking information before initiating transfers.
### For Administrators
To process bank transfer payments:
1. **Access Orders**: Navigate to **Billing > Orders** in admin panel to view all orders
2. **Filter Pending Orders**: Select "Pending" status from the filter options to view all orders awaiting manual verification (including bank transfers and other manual payment methods)
3. **Review Order Details**: Click on individual orders to view customer and payment information
4. **Verify Payments**: Check your bank account for incoming transfers matching order details
5. **Update Order Status**: Use **Approve** or **Reject** buttons to update order status
6. **Process Orders**: Approved orders are processed immediately and cannot be reverted
Bank transfer payments require manual verification. Always confirm payment receipt before fulfilling orders. Once approved or rejected, orders cannot be reverted.
## Order Management
### Admin Order Management
Bank transfer orders appear in the admin panel with "Pending" status until manually verified. Note that the pending orders list includes all manual payment methods, not just bank transfers:
1. **Access Orders**: Navigate to **Billing > Orders** in the admin panel to view all orders
2. **Filter Pending Orders**: Select "Pending" status from the filter options to view all orders awaiting manual verification (including bank transfers and other manual payment methods)
3. **Review Order Details**: Click on any order to view:
* Customer information and workspace details
* Order summary (plan, credits, total amount)
* Payment gateway information (Yookassa transaction ID)
* Order creation date and status
### Order Status Actions
For each pending bank transfer order, you can:
* **Approve**: Click the green "Approve" button to confirm payment and fulfill the order
* **Reject**: Click the gray "Reject" button to cancel the order if payment is not received
### Customer Order Tracking
Customers can track their bank transfer orders through their billing overview:
1. **Access Billing**: Navigate to billing overview from the main dashboard
2. **View Orders**: Check the "Latest orders" section for pending bank transfer orders
3. **Order Details**: Click on any order to view detailed bank transfer instructions
4. **Status Updates**: Order status updates automatically when admin approves payment
### Customer Checkout Experience
When customers select bank transfer as their payment method:
1. **Payment Method Selection**: During checkout, customers can choose "Bank Transfer" from available payment options
2. **Order Placement**: After selecting bank transfer, customers click "Place order" to complete the purchase
3. **Order Confirmation**: The system creates a pending order and redirects to order details page
4. **Bank Transfer Instructions**: Customers receive comprehensive banking details including:
* Order ID for reference
* Beneficiary information (company name, account details)
* Bank details (name, routing number, SWIFT code)
* Complete addresses for both beneficiary and bank
* Correspondent bank information (for international transfers)
* Download option for bank requisites
The checkout page displays "Payment details will be provided after placing the order" to inform customers that banking information comes after order creation.
## Best Practices
### Security Considerations
* **Verify Payments**: Always confirm payment receipt before order fulfillment
* **Document Everything**: Keep records of all bank transfer transactions
* **Secure Information**: Protect sensitive banking information
* **Regular Reconciliation**: Regularly reconcile bank statements with orders
### Customer Communication
* **Clear Instructions**: Provide detailed, easy-to-follow transfer instructions
* **Reference Numbers**: Include order numbers in transfer references
* **Timeline Expectations**: Set clear expectations for payment processing time
* **Support Contact**: Provide customer support for payment-related questions
### Documentation Requirements
* **Bank Statements**: Keep copies of bank statements for verification
* **Transfer Confirmations**: Save transfer confirmation documents
* **Order Records**: Maintain detailed order and payment records
* **Compliance**: Ensure compliance with local banking regulations
## Troubleshooting
### Common Issues
**Payment Not Received**
* Verify bank account details are correct
* Check if transfer is still in progress
* Contact your bank for transfer status
**Incorrect Transfer Details**
* Verify customer provided correct reference information
* Check if transfer was sent to correct account
* Contact customer for clarification
**International Transfer Delays**
* International transfers can take 1-5 business days
* Check with your bank for processing times
* Verify all international banking codes are correct
### Verification Process
1. **Check Bank Account**: Review recent transactions in your bank account
2. **Match Details**: Compare transfer details with order information in admin panel
3. **Verify Order ID**: Ensure the order ID matches the reference provided to customer
4. **Contact Bank**: If needed, contact your bank for verification
5. **Update Order Status**: Use the "Approve" button in admin panel to confirm payment
### Order Status Management
**Pending Orders**
* Orders remain in "Pending" status until manually approved
* Customers cannot use purchased credits until order is approved
* Orders can be rejected if payment is not received within reasonable time
**Approved Orders**
* Orders are processed immediately upon approval
* Credits are immediately available to customers
* Orders cannot be reverted or reversed once approved
**Rejected Orders**
* Orders are cancelled and credits are not provided
* Orders cannot be reverted or reversed once rejected
* Customers should be notified of rejection with reason
* New orders can be placed if payment issues are resolved
Bank Transfer payments are now configured and ready to accept customer payments through direct bank transfers.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Cryptomus Integration
Source: https://docs.aikeedo.com/integrations/payment-gateways/cryptomus
Configure Cryptomus payment gateway in Aikeedo with API credentials for secure cryptocurrency payments and digital asset transactions.
Cryptomus integration enables secure cryptocurrency payment processing for your Aikeedo platform, supporting various digital currencies and blockchain networks. This guide covers account setup and API configuration.
Cryptomus integration is available as a separate plugin that must be purchased from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content) before you can configure this payment gateway.
## Prerequisites
Before you begin, ensure you have:
* An active [Cryptomus merchant account](https://cryptomus.com/gateway)
* Access to your Aikeedo admin panel
* Your domain properly configured with SSL enabled
* A verified business entity
## Step 1: Set Up Your Cryptomus Account
If you haven't already, sign up for a Cryptomus merchant account:
1. Visit the [Cryptomus website](https://cryptomus.com/gateway) and click "Get Started"
2. Register using your email or phone number
3. Create a merchant account for payment processing
4. Complete the business verification process
5. Wait for account approval and moderation
Cryptomus requires domain verification to ensure the legitimacy of your platform. This can be done through DNS records, meta tags, or HTML file upload.
## Step 2: Obtain Cryptomus API Credentials
To connect Aikeedo with Cryptomus, you'll need to retrieve your API credentials:
1. Log in to your [Cryptomus Dashboard](https://cryptomus.com/dashboard)
2. Navigate to **Business** > **Merchants**
3. Select your project and click **Merchant Settings**
4. In the **API Integration** section, you'll find your credentials:
* **Merchant ID**: Your unique merchant identifier (e.g., `1bd8163d-92e0-4c1d-9452-5b4dc212f307`)
* **Payment API Key**: Your API key for payment operations (e.g., `Dgke********3stU`)
Keep your Payment API Key confidential! Never share it publicly or include it in client-side code.
## Step 3: Pass KYB Verification and Domain Verification
Before you can process payments, you need to pass Cryptomus verification:
1. **KYB (Know Your Business) Verification**:
* Click "Pass KYB" in your merchant settings
* Provide required business documents and information
* This ensures your safety by preventing fraud in financial transactions
2. **Domain Verification**: Choose one of these methods:
* **DNS Record**: Add a TXT record to your domain
* **Meta Tag**: Insert a meta tag in your website's head section
* **HTML File**: Upload an HTML file to your website root
3. **Wait for Approval**: Cryptomus will review your application
Both KYB verification and domain verification are mandatory for security and compliance purposes. Without them, your merchant account cannot process payments.
## Step 3: Configure Cryptomus in Aikeedo
Now that you have your Cryptomus account set up, configure the integration in Aikeedo:
1. Log in to your Aikeedo admin panel
2. Navigate to **Payments** in the main menu
3. Click on **Cryptomus**
### Details Section
Configure the general settings:
1. **Status**: Toggle the switch to enable Cryptomus payments
2. **Charge Currency**: Select your preferred currency
* **Inherit**: Use your platform's default currency (USD)
* **Custom**: Choose from a wide range of fiat currencies including USD, EUR, GBP, and many others
* If you select a currency different from your default and have a currency rate provider set up, amounts will be automatically converted
3. **Enabled for** (required field):
* **One-time payments**: Single transactions only
* **One-time and monthly payments**: Both single and recurring subscriptions (recommended)
### Keys Section
Enter your API credentials:
1. **Merchant ID**: Enter your Cryptomus Merchant ID (e.g., `1bd8163d-92e0-4c1d-9452-5b4dc212f307`)
2. **Payment API Key**: Enter your Cryptomus Payment API Key
3. Click **Save changes** to apply your settings
Congratulations! Cryptomus payments are now enabled on your Aikeedo platform.
## Testing Your Integration
Before going live, it's crucial to test your Cryptomus integration:
1. Create test plans with small prices to verify payment processing
2. Test different cryptocurrency networks
3. Test payment confirmation and order updates
Always test thoroughly with small amounts before processing larger payments. Cryptocurrency transactions are irreversible.
## Troubleshooting
If you encounter any issues with your Cryptomus integration, try these steps:
1. **Verify API Credentials**: Double-check your Merchant ID and Payment API Key in the Keys section
2. **Check KYB Verification**: Ensure your merchant account has passed KYB verification in the merchant settings
3. **Check Domain Verification**: Ensure your domain is properly verified
4. **Test Payments**: Create test plans with small amounts to verify the integration works correctly
### Common Issues
Double-check your Merchant ID and Payment API Key. You can find these in your Cryptomus Dashboard under Business > Merchants > Merchant Settings.
Ensure your merchant account has passed KYB verification and domain verification. Check that your domain is properly verified and your merchant account has passed moderation.
If you see "KYB not verified" in your merchant settings, click "Pass KYB" and provide the required business documents and information to complete the verification process.
Cryptomus does not support annual payments. Only one-time payments and monthly subscriptions are available. This limitation is clearly indicated in the settings interface.
## Additional Resources
* [Cryptomus API Documentation](https://doc.cryptomus.com/)
* [Cryptomus Business Dashboard](https://cryptomus.com/dashboard)
* [Supported Cryptocurrencies](https://cryptomus.com/currencies)
By following this guide, you've successfully integrated Cryptomus payments into your Aikeedo platform, providing your users with a secure and convenient cryptocurrency payment experience.
# Manual Payment Method
Source: https://docs.aikeedo.com/integrations/payment-gateways/custom-payment-method
Configure custom manual payment methods in Aikeedo with custom instructions, file uploads, and flexible payment processing for unique business requirements.
Manual Payment Method is a built-in payment option in Aikeedo that allows you to create custom payment instructions for customers. This flexible payment method is perfect for unique business requirements, offline payments, or specific payment processes that don't fit standard payment gateways.
## Overview
Custom Payment Method provides complete flexibility in how customers pay for your services. You can define custom payment instructions, upload supporting files, and create payment flows that match your specific business needs.
This payment method is ideal for:
* Offline payment processes
* Custom payment instructions
* Business-specific payment requirements
* Alternative payment methods not covered by standard gateways
* Manual payment verification processes
Manual Payment Method is a built-in payment option and doesn't require additional plugins or external service setup.
## Prerequisites
Before configuring manual payment methods, ensure you have:
* Clear payment instructions for your customers
* Any supporting documentation or files ready for upload
* A defined process for verifying custom payments
* Customer support procedures for payment-related inquiries
## Configuration
### Accessing Manual Payment Settings
1. Log in to your Aikeedo admin panel
2. Navigate to **Settings > Payments > Manual Payment** (or **Settings > Payments > \[Your Method Name]** if already configured)
3. You'll see the "Manual payment settings" configuration page
The navigation path changes based on configuration status:
* **Before configuration**: **Settings > Payments > Manual Payment**
* **After configuration**: **Settings > Payments > \[Your Method Name]** (where \[Your Method Name] is the method name you've defined)
### Basic Configuration
Configure the fundamental settings for your custom payment method:
Toggle the **Status** switch to "Enabled" to activate custom payment processing.
Set the **Enabled for** dropdown to specify which payment types support custom payment methods:
* One time payments (Recommended)
* One-time and annual payments
* All payments
Choose **One time payments (Recommended)** from the dropdown for standard custom payment processing.
Since custom payment methods require manual verification, it's recommended to enable them only for one-time payments to avoid complications with recurring payment processing.
### Payment Method Details
Define how your custom payment method appears to customers:
Enter a **Method name** that will be displayed to customers at checkout:
* Use a clear, descriptive name (e.g., "Check Payment", "Money Order", "Corporate Purchase Order")
* This name appears in the payment options during checkout
* The navigation path will change to **Settings > Payments > \[Your Method Name]** after saving
* Keep it concise but informative
Provide detailed **Payment instructions** for customers:
* Enter step-by-step instructions customers should follow
* Include all necessary details for payment completion
* Specify any required information or documentation
* Mention processing times and next steps
Be as detailed as possible. Include contact information, reference numbers, and any specific requirements.
### Optional File Upload
Enhance your payment instructions with supporting documentation:
Add an optional **Instructions file** to provide additional guidance:
* Click **Browse files** to upload supporting documents
* Upload PDFs, images, or other relevant files
* Use **View current** to review uploaded files
* Use **Delete** to remove outdated files
Supported file types typically include PDF, DOC, DOCX, JPG, PNG, and other common formats.
## Payment Processing Flow
### For Customers
When customers choose your custom payment method:
1. **Payment Selection**: Customer selects your custom payment method from available payment methods during checkout
2. **Order Placement**: Customer clicks "Place order" to complete the purchase
3. **Payment Instructions**: Customer is redirected to order details page with your custom payment instructions
4. **Transfer Execution**: Customer follows your specific payment process using provided instructions
5. **Order Tracking**: Customer can monitor order status in their billing overview
6. **Order Fulfillment**: Order is processed after payment verification by admin
Payment details are provided after placing the order, not during checkout. Customers are immediately redirected to the order details page where they can view complete payment instructions for initiating payments.
### For Administrators
To process custom payment method orders:
1. **Access Orders**: Navigate to **Billing > Orders** in admin panel to view all orders
2. **Filter Pending Orders**: Select "Pending" status from the filter options to view all orders awaiting manual verification (including custom payment methods and other manual payment methods)
3. **Review Order Details**: Click on individual orders to view customer and payment information
4. **Verify Payments**: Confirm payment completion through your defined process
5. **Update Order Status**: Use **Approve** or **Reject** buttons to update order status
6. **Process Orders**: Approved orders are processed immediately and cannot be reverted
Custom payment methods require manual verification. Always confirm payment completion before fulfilling orders. Once approved or rejected, orders cannot be reverted.
## Order Management
### Admin Order Management
Custom payment method orders appear in the admin panel with "Pending" status until manually verified. Note that the pending orders list includes all manual payment methods, not just custom payment methods:
1. **Access Orders**: Navigate to **Billing > Orders** in the admin panel to view all orders
2. **Filter Pending Orders**: Select "Pending" status from the filter options to view all orders awaiting manual verification (including custom payment methods and other manual payment methods)
3. **Review Order Details**: Click on any order to view:
* Customer information and workspace details
* Order summary (plan, credits, total amount)
* Payment method information
* Order creation date and status
### Order Status Actions
For each pending custom payment method order, you can:
* **Approve**: Click the green "Approve" button to confirm payment and fulfill the order
* **Reject**: Click the gray "Reject" button to cancel the order if payment is not received
### Customer Order Tracking
Customers can track their custom payment method orders through their billing overview:
1. **Access Billing**: Navigate to billing overview from the main dashboard
2. **View Orders**: Check the "Latest orders" section for pending custom payment method orders
3. **Order Details**: Click on any order to view detailed payment instructions
4. **Status Updates**: Order status updates automatically when admin approves payment
### Customer Checkout Experience
When customers select your custom payment method as their payment method:
1. **Payment Method Selection**: During checkout, customers can choose your custom payment method from available payment options
2. **Order Placement**: After selecting the custom payment method, customers click "Place order" to complete the purchase
3. **Order Confirmation**: The system creates a pending order and redirects to order details page
4. **Payment Instructions**: Customers receive comprehensive payment instructions including:
* Order ID for reference
* Custom payment instructions you've configured
* Any uploaded instruction files
* Contact information for support
The checkout page displays "Payment details will be provided after placing the order" to inform customers that payment instructions come after order creation.
## Best Practices
### Instruction Writing
**Clear and Concise**
* Use simple, easy-to-understand language
* Break complex processes into numbered steps
* Include all necessary contact information
* Specify exact amounts and reference numbers
**Complete Information**
* Include all required payment details
* Specify processing times and deadlines
* Mention any required documentation
* Provide multiple contact methods
### Customer Communication
**Proactive Support**
* Provide clear contact information for questions
* Set realistic expectations for processing times
* Offer multiple ways to reach support
* Follow up on pending payments
**Documentation**
* Keep records of all custom payment transactions
* Maintain copies of payment confirmations
* Document any special arrangements
* Track payment processing times
### Security Considerations
**Payment Verification**
* Always verify payment completion before order fulfillment
* Use secure methods for payment confirmation
* Protect sensitive customer information
* Maintain audit trails for all transactions
**Documentation Security**
* Secure storage of payment-related files
* Regular backup of payment records
* Access controls for sensitive information
* Compliance with data protection regulations
## Common Use Cases
### Offline Payments
* Cash payments for local customers
* Check payments for B2B transactions
* Money order or cashier's check payments
* In-person payment collection
### Specialized Payment Methods
* Cryptocurrency payments
* Wire transfers with specific instructions
* Payment plans or installments
* Corporate purchase orders
### Regional Payment Methods
* Local payment systems not supported by standard gateways
* Bank-specific payment processes
* Government or institutional payment methods
* Alternative financial services
## Troubleshooting
### Common Issues
**Unclear Instructions**
* Review and simplify payment instructions
* Test instructions with a sample customer
* Gather feedback and improve clarity
* Provide multiple contact methods for questions
**Payment Verification Delays**
* Establish clear verification procedures
* Set up automated notifications for new orders
* Create escalation procedures for delayed payments
* Maintain regular communication with customers
**File Upload Issues**
* Check file size and format requirements
* Ensure files are accessible and properly formatted
* Test file uploads before going live
* Provide alternative access methods if needed
### Order Status Management
**Pending Orders**
* Orders remain in "Pending" status until manually approved
* Customers cannot use purchased credits until order is approved
* Orders can be rejected if payment is not received within reasonable time
**Approved Orders**
* Orders are processed immediately upon approval
* Credits are immediately available to customers
* Orders cannot be reverted or reversed once approved
**Rejected Orders**
* Orders are cancelled and credits are not provided
* Orders cannot be reverted or reversed once rejected
* Customers should be notified of rejection with reason
* New orders can be placed if payment issues are resolved
Manual Payment Method is now configured and ready to accept payments through your custom payment process.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Iyzico Integration
Source: https://docs.aikeedo.com/integrations/payment-gateways/iyzico
Configure the Iyzico payment gateway in Aikeedo via UI: enter keys, set currency, and add webhooks. No coding required.
Iyzico enables secure card payments and subscriptions in Turkey. This guide shows how to configure the ready‑to‑use plugin in Aikeedo using your Iyzico dashboard. No code is needed.
Iyzico integration is available as a separate plugin that must be purchased from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content) before you can configure this payment gateway.
Subscription management (Abonelik Yönetimi) is provided by Iyzico as an add‑on. To use subscriptions, enable it from the Eklentiler section in your Iyzico dashboard. Learn more: [Iyzico Abonelik](https://docs.iyzico.com/urunler/abonelik).
## Prerequisites
Make sure you have:
* An active [Iyzico merchant account](https://www.iyzico.com/)
* Access to your Aikeedo admin panel
* Your domain with SSL (HTTPS)
* A business registered in Turkey (required by Iyzico)
## Step 1: Get your Iyzico API keys
From the Iyzico dashboard:
1. Log in to the [Iyzico Merchant Panel](https://merchant.iyzipay.com/login) or Sandbox panel if testing.
2. Go to **Ayarlar > Firma Ayarları** (Settings > Merchant Settings).
3. In the **API Anahtarları** card you will see:
* **API Anahtarı** (API Key) — public key
* **Güvenlik Anahtarı** (Secret Key)
4. Copy both values. You will paste them into Aikeedo.
Keep your Secret Key confidential. Do not share it publicly.
## Step 2: Configure Iyzico in Aikeedo
1. In Aikeedo, open **Payments** and click **Iyzico**.
2. In the **Details** section:
* **Status**: Enable to turn on Iyzico.
* **Charge Currency**: Choose the currency to charge customers. Options include: Turkish Lira (TRY), US Dollar (USD), Euro (EUR), British Pound (GBP), Norwegian Krone (NOK), Russian Ruble (RUB), Swiss Franc (CHF), or **Inherit** to use your default.
* **Mode**: Select **Sandbox** for testing or **Live** for production.
3. In the **Keys** section (Sandbox or Live tab):
* **API public key**: paste your Iyzico API Key.
* **Secret key**: paste your Iyzico Secret Key.
4. In the **Webhooks** card, Aikeedo shows your read‑only webhook URL.
* Copy this URL; you will paste it into the Iyzico dashboard in the next step.
5. Click **Save changes**.
Iyzico is now connected. New payments will be processed through Iyzico according to your selected mode and currency.
## Step 3: Add webhook URLs in Iyzico
To receive payment and subscription notifications from Iyzico:
1. In the Iyzico dashboard, open **Ayarlar > Firma Ayarları**.
2. In **İşyeri Bildirimleri** (Merchant Notifications), enable notifications and paste the webhook URL you copied from Aikeedo into the **İşyeri Bildirimleri Url** field.
3. If you use subscriptions, in **İşyeri Abonelik Ödeme Bildirimleri** enable both toggles and paste the same webhook URL into:
* **Başarılı Abonelik Bildirimleri Url**
* **Başarısız Abonelik Bildirimleri Url**
4. Save changes in the Iyzico dashboard.
Using the same single webhook URL for all merchant and subscription events is recommended. Aikeedo routes events automatically.
## Testing
To test without real charges:
1. In Aikeedo, set **Mode** to **Test**.
2. Log in to the [Iyzico Sandbox](https://sandbox-merchant.iyzipay.com/).
3. Use Iyzico test cards to perform a payment or a subscription: [Test Kartları](https://docs.iyzico.com/ek-bilgiler/test-kartlari).
4. Check that the transaction appears in Aikeedo and that webhooks are received.
Sandbox uses test cards and does not move money. Switch to Live only after successful tests.
## Going live
1. Replace Sandbox keys in Aikeedo with your Live **API Key** and **Secret Key** from Iyzico.
2. Ensure **İşyeri Bildirimleri** and (if applicable) **Abonelik Ödeme Bildirimleri** point to your production domain.
3. Change **Mode** to **Live** and save.
## Troubleshooting
Verify you pasted the correct key pair (API Key and Güvenlik Anahtarı) for the selected mode (Sandbox/Live).
Confirm notifications are enabled in Iyzico and that the URL matches `{your_domain}/webhooks/iyzico` over HTTPS.
If a currency rate provider is configured in Aikeedo, amounts will be converted to the selected Charge Currency automatically.
Ensure the Iyzico Abonelik add‑on is purchased and enabled under Eklentiler in the Iyzico dashboard, and subscription notification URLs are set.
## Additional resources
* [Iyzico Abonelik (official docs)](https://docs.iyzico.com/urunler/abonelik)
* [Iyzico Merchant Panel – Sandbox](https://sandbox-merchant.iyzipay.com/)
* [Iyzico Test Cards](https://docs.iyzico.com/ek-bilgiler/test-kartlari)
* [Iyzico Website](https://www.iyzico.com/)
# Mercado Pago
Source: https://docs.aikeedo.com/integrations/payment-gateways/mercadopago
Configure Mercado Pago as a payment gateway in Aikeedo to accept one-time and recurring payments across Latin America.
The Mercado Pago plugin adds payment acceptance for Latin America to your Aikeedo platform. It supports one-time purchases and recurring subscription billing across seven regional currencies, using Mercado Pago's hosted checkout and Preapproval subscription flows.
Mercado Pago is an additional plugin available for purchase from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/mercadopago/?utm_source=docs\&utm_medium=content). Install it from your admin panel before following this guide.
## Prerequisites
* A [Mercado Pago](https://www.mercadopago.com/) business account
* The Mercado Pago plugin installed on your Aikeedo installation
* Access to your Aikeedo admin panel
## Supported currencies
| Currency | Country |
| - | - |
| ARS | Argentina |
| BRL | Brazil |
| CLP | Chile |
| COP | Colombia |
| MXN | Mexico |
| PEN | Peru |
| UYU | Uruguay |
## Step 1: Obtain your access tokens
You need separate **Access Token** values for test and live modes. Refer to the [Mercado Pago credentials documentation](https://www.mercadopago.com.ar/developers/en/docs/subscriptions/additional-content/your-integrations/credentials) for instructions on how to find and activate your credentials.
Keep your access tokens confidential. Never share them publicly or commit them to version control.
## Step 2: Set up the webhook and obtain the signing secret
Aikeedo verifies incoming Mercado Pago webhooks using an HMAC-SHA256 signature. You need to register your webhook URL and obtain the signing secret Mercado Pago provides.
1. In the Mercado Pago Developer Dashboard, navigate to **Notifications > Webhooks**.
2. Click **Add webhook**.
3. Set the notification URL to:
```
https://your-aikeedo-domain.com/webhooks/mercadopago
```
4. Under **Events**, select all events, or at minimum **Subscription preapproval** (`subscription_preapproval`) to receive subscription lifecycle events.
5. Save the webhook. Mercado Pago will display a **signing secret** — copy it.
6. Repeat this for both your test and live webhook configurations to obtain separate signing secrets for each mode.
## Step 3: Enable Mercado Pago in Aikeedo
1. In your Aikeedo admin panel, navigate to **Settings > Payments > Mercado Pago**.
2. Toggle the switch to enable Mercado Pago.
3. Set the **Charge Currency** — select a specific currency or leave it as **Inherit** to use the platform's default currency.
4. Select the **Mode**: **Test** for development, **Live** for production.
5. Enter your credentials for each mode:
* **Live**: live access token and live webhook signing secret
* **Test**: test access token and test webhook signing secret
6. Click **Save changes**.
Mercado Pago is now enabled on your platform.
## Payment flows
**One-time payments** — Aikeedo creates a Checkout Preference and redirects the customer to the Mercado Pago-hosted checkout page. After payment, the customer is redirected back to your platform and the payment is verified against the order.
**Recurring subscriptions** — Aikeedo creates a Mercado Pago Preapproval and redirects the customer to authorize it. Subsequent billing cycles are charged automatically on a monthly or yearly cadence. When a subscription is cancelled in Aikeedo, it is cancelled in Mercado Pago via the API. If Mercado Pago sends a `cancelled` or `paused` status via webhook, Aikeedo cancels the subscription on its side as well.
## Testing
1. Set the mode to **Test** in **Settings > Payments > Mercado Pago**.
2. Use [Mercado Pago test users and cards](https://www.mercadopago.com/developers/en/docs/checkout-pro/additional-content/your-integrations/test/cards) to simulate payment scenarios.
3. Verify that one-time payments and subscriptions complete correctly and that webhook events are received and processed.
Switch to **Live** mode when you are ready to accept real payments.
## Troubleshooting
* **Payments not completing**: Verify that the access token matches the selected mode (test/live) and that the application credentials are active.
* **Webhooks not received**: Confirm that the webhook URL is publicly accessible and that the `subscription_preapproval` event type is selected in the Mercado Pago developer dashboard.
* **Webhook signature verification failing**: Ensure the **Webhook Signing Secret** in Aikeedo matches the secret shown in the Mercado Pago dashboard for the registered webhook endpoint.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Payment Gateways
Source: https://docs.aikeedo.com/integrations/payment-gateways/overview
Configure payment gateways for secure online payments in Aikeedo. Choose from built-in options like Stripe, PayPal, Bank Transfer, and Manual Payment, or purchase additional gateway plugins.
## Overview
Aikeedo provides comprehensive payment processing capabilities through multiple payment gateways, enabling you to accept payments from customers worldwide. The platform includes built-in payment methods and supports additional gateway plugins for specialized payment processing needs.
Built-in payment methods are included with Aikeedo, while additional gateway
plugins are available for purchase from the [Aikeedo
Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content).
## Built-in Payment Methods
Aikeedo includes several payment methods out of the box:
Industry-leading payment processor with support for cards, digital wallets, and international payments.
Popular payment platform supporting PayPal accounts, credit cards, and PayPal
Credit.
Direct bank transfer payments with domestic and international transfer
support.
Custom payment method with flexible instructions and manual verification process.
## Additional Gateway Plugins
Expand your payment options with specialized gateway plugins available in the marketplace:
Russian payment gateway supporting cards, e-wallets, and mobile payments.
African payment gateway with support for local payment methods across Africa.
Indian payment gateway supporting cards, UPI, net banking, and digital
wallets.
Turkish payment gateway with comprehensive local payment method support.
Cryptocurrency payment gateway supporting Bitcoin, Ethereum, and other digital
currencies.
Payment gateway for Latin America supporting one-time and recurring payments
in ARS, BRL, CLP, COP, MXN, PEN, and UYU.
Payment gateway for Southeast Asia supporting one-time and recurring payments in IDR, PHP, VND, THB, SGD, and MYR.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# PayPal Integration
Source: https://docs.aikeedo.com/integrations/payment-gateways/paypal
Configure PayPal payment gateway in Aikeedo with API credentials, webhooks, and subscription handling for secure online payments.
PayPal integration enables secure payment processing and subscription management for your Aikeedo platform. This guide covers account setup, API configuration, and webhook implementation.
## Prerequisites
Before you begin, ensure you have:
* An active [PayPal Business account](https://www.paypal.com/us/business)
* Access to your Aikeedo admin panel
* Your domain properly configured with SSL enabled
## Step 1: Set Up Your PayPal Business Account
If you haven't already, sign up for a PayPal Business account:
1. Visit the [PayPal Business website](https://www.paypal.com/us/business) and click "Sign Up"
2. Follow the prompts to create and verify your business account
3. Complete any additional verification steps required by PayPal
## Step 2: Obtain PayPal API Credentials
To connect Aikeedo with PayPal, you'll need to retrieve your API credentials:
1. Log in to the [PayPal Developer Dashboard](https://developer.paypal.com/dashboard/)
2. Navigate to **Apps & Credentials**
3. Click on **Create App** to generate a new app for your integration
4. Set the following details:
* App Name: Choose a name for your integration (e.g., "Aikeedo Payments")
* Select "Merchant" as the app type
5. Click "Create App"
6. On the next page, you'll find your **Client ID** and can generate a **Secret Key**
Keep your Secret Key confidential! Never share it publicly or include it in client-side code.
## Step 3: Set Up PayPal Webhooks
Webhooks are crucial for handling recurring payments and keeping your Aikeedo platform in sync with PayPal. Here's how to set them up:
1. In the PayPal Developer Dashboard, go to your app's details page
2. Scroll down to the **Webhooks** section and click "Add Webhook"
3. Enter your webhook URL: `https://your-aikeedo-domain.com/webhooks/paypal`
4. Select the following events:
* `INVOICING.INVOICE.CANCELLED`
* `INVOICING.INVOICE.CREATED`
* `INVOICING.INVOICE.PAID`
* `INVOICING.INVOICE.REFUNDED`
* `INVOICING.INVOICE.SCHEDULED`
* `INVOICING.INVOICE.UPDATED`
* `BILLING.SUBSCRIPTION.CANCELLED`
5. Click "Save" to create the webhook
6. After creation, you'll see a "Webhook ID" - copy this for later use
If you're not sure which events to select, it's safe to choose all relevant billing and invoicing events for comprehensive coverage.
## Step 4: Enable PayPal in Aikeedo
Now that you have your PayPal account set up, it's time to configure Aikeedo:
1. Log in to your Aikeedo admin panel
2. Navigate to **Settings > Payments > PayPal**
3. Toggle the switch to enable PayPal payments
4. Choose the appropriate mode:
* **Sandbox**: For testing payments
* **Live**: For processing real transactions
5. Enter the following details:
* **Client ID**: Your PayPal app's Client ID
* **API Secret Key**: Your PayPal app's Secret Key
* **Webhook ID**: The Webhook ID from your webhook setup
6. Set your preferred **Charge Currency**
7. Click "Save changes" to apply your settings
Congratulations! PayPal payments are now enabled on your Aikeedo platform.
## Testing Your Integration
Before going live, it's crucial to test your PayPal integration:
1. Set Aikeedo to "Sandbox" mode
2. Use [PayPal's sandbox accounts](https://developer.paypal.com/tools/sandbox/accounts/) to simulate various payment scenarios
3. Create a test subscription or product purchase
4. Verify that payments are processed correctly and webhooks are functioning
Once you're confident everything is working as expected, switch to "Live" mode to start accepting real payments.
## Troubleshooting
If you encounter any issues with your PayPal integration, try these steps:
1. Double-check all API credentials and Webhook ID for accuracy
2. Ensure your webhook URL is correct and accessible
3. Review PayPal's developer dashboard for any error logs or failed webhook attempts
4. Check Aikeedo's error logs for any integration-related issues
By following this guide, you've successfully integrated PayPal payments into your Aikeedo platform, providing your users with a secure and convenient payment experience.
# Paystack Integration
Source: https://docs.aikeedo.com/integrations/payment-gateways/paystack
Configure Paystack payment gateway in Aikeedo with API credentials and subscription handling for secure online payments across Africa.
Paystack integration enables secure payment processing and subscription management for your Aikeedo platform, specifically designed for African markets. This guide covers account setup and API configuration.
Paystack integration is available as a separate plugin that must be purchased from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content) before you can configure this payment gateway.
## Prerequisites
Before you begin, ensure you have:
* An active [Paystack merchant account](https://paystack.com/)
* Access to your Aikeedo admin panel
* Your domain properly configured with SSL enabled
* A valid business registration in supported African countries
## Step 1: Set Up Your Paystack Account
If you haven't already, sign up for a Paystack merchant account:
1. Visit the [Paystack website](https://paystack.com/) and click "Create a free account"
2. Follow the prompts to create and verify your merchant account
3. Complete the business verification process with required documents
4. Wait for account approval (typically 1-2 business days)
Paystack requires business registration in supported African countries. Personal accounts are not supported for merchant services.
## Step 2: Obtain Paystack API Credentials
To connect Aikeedo with Paystack, you'll need to retrieve your API credentials:
1. Log in to your [Paystack Dashboard](https://dashboard.paystack.com/)
2. Navigate to **Settings** > **API Keys & Webhooks**
3. You'll find your credentials:
* **Public Key**: Starts with `pk_test_` (test) or `pk_live_` (live)
* **Secret Key**: Starts with `sk_test_` (test) or `sk_live_` (live)
Keep your Secret Key confidential! Never share it publicly or include it in client-side code.
## Step 3: Configure Paystack in Aikeedo
Now that you have your Paystack account set up, configure the integration in Aikeedo:
1. Log in to your Aikeedo admin panel
2. Navigate to **Settings > Payments** page
3. Click on **Paystack**
### Details Section
Configure the general settings:
1. **Status**: Toggle the switch to enable Paystack payments
2. **Charge Currency**: Select your preferred currency
* **Inherit**: Use your platform's default currency
* **Nigerian Naira (NGN)**: Default for Nigeria
* **Ghanaian Cedi (GHS)**: For Ghana
* **Kenyan Shilling (KES)**: For Kenya
* **South African Rand (ZAR)**: For South Africa
* **US Dollar (USD)**: International transactions
* If you select a currency different from your default and have a currency rate provider set up, amounts will be automatically converted
3. **Mode**: Choose between:
* **Test**: For testing payments with Paystack's test environment
* **Live**: For processing real transactions
### Keys Section
Enter your API credentials:
1. **API public key**: Enter your Paystack public key (starts with `pk_test_` for test mode or `pk_live_` for live mode)
2. **API secret key**: Enter your Paystack secret key (starts with `sk_test_` for test mode or `sk_live_` for live mode)
3. Click **Save changes** to apply your settings
### Webhooks Section
Configure webhook notifications:
1. **Webhook URL**: The webhook URL is automatically set to `{your_domain}/webhooks/paystack`
2. Copy this URL and add it to your Paystack Dashboard:
* Go to **Settings** > **API Keys & Webhooks** in your Paystack Dashboard
* Enter the webhook URL in the **Test Webhook URL** field (or **Live Webhook URL** for production)
* Click **Save changes** in your Paystack Dashboard
The webhook URL will automatically handle all necessary payment events including successful payments, failed payments, and subscription updates. No additional configuration is required in Aikeedo.
Congratulations! Paystack payments are now enabled on your Aikeedo platform.
## Testing Your Integration
Before going live, it's crucial to test your Paystack integration:
1. Set the Paystack payment gateway to "Test" mode in the Details section
2. Use [Paystack's test card numbers](https://paystack.com/docs/payments/test-payments) to simulate various payment scenarios:
3. Create a test subscription or product purchase
4. Verify that payments are processed correctly
Paystack test environment uses special test card numbers and doesn't process real payments. Always test thoroughly before switching to live mode.
## Troubleshooting
If you encounter any issues with your Paystack integration, try these steps:
1. **Verify API Credentials**: Double-check your API Public Key and Secret Key in the Keys section
2. **Check Mode Setting**: Ensure you're using Test mode for testing and Live mode for production
3. **Verify Currency**: Make sure your selected currency is supported by Paystack
4. **Test Payments**: Use Paystack's test card numbers to verify the integration works correctly
5. **Check Webhook Configuration**: Ensure your webhook URL is properly configured in both Aikeedo and Paystack
### Common Issues
Double-check your API Public Key and Secret Key. You can find these in your Paystack Dashboard under Settings > API Keys & Webhooks.
Ensure you're using the correct mode (Test for testing, Live for production) and that your currency is supported.
Verify that your webhook URL is properly configured in your Paystack Dashboard and that your domain has SSL enabled.
If you have a currency rate provider set up, ensure it supports the currencies you're using. The amount will be automatically converted to your chosen charge currency.
## Additional Resources
* [Paystack Developer Documentation](https://paystack.com/docs)
* [Paystack API Reference](https://paystack.com/docs/api/)
* [Webhook Events Documentation](https://paystack.com/docs/api/#events)
* [Test Payments Guide](https://paystack.com/docs/payments/test-payments)
* [Supported Countries and Currencies](https://paystack.com/docs/payments/currencies)
By following this guide, you've successfully integrated Paystack payments into your Aikeedo platform, providing your users with a secure and convenient payment experience tailored for African markets.
# Razorpay Integration
Source: https://docs.aikeedo.com/integrations/payment-gateways/razorpay
Configure the Razorpay plugin in Aikeedo using API keys and webhooks.
Razorpay enables secure payments and subscriptions for the Indian market. This guide shows you how to fetch keys from Razorpay and paste them into Aikeedo.
Razorpay integration is available as a separate plugin that must be purchased from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content) before you can configure this payment gateway.
## Prerequisites
Before you begin, ensure you have:
* An active [Razorpay merchant account](https://razorpay.com/)
* Access to your Aikeedo admin panel
* Your domain using HTTPS (valid SSL certificate)
* Business eligibility for accepting payments in India
## Step 1: Set up your Razorpay account
If you are new to Razorpay:
1. Visit the [Razorpay website](https://razorpay.com/) and click Sign Up”.
2. Complete onboarding and KYC verification.
3. Wait for account approval (usually 1–2 business days).
## Step 2: Get your API keys in Razorpay
1. Log in to the [Razorpay Dashboard](https://dashboard.razorpay.com/).
2. Select the mode at the top: **Test** or **Live**.
3. Go to **Account & Settings → API Keys** and click **Generate Key** for the selected mode.
4. Copy the following from the pop‑up:
* **Key Id** (starts with `rzp_test_` in Test or `rzp_live_` in Live)
* **Key Secret** (shown once when generated)
Keep your Key Secret confidential. It is shown only once on generation; store it securely.
Generating **Live** keys may require your website/app details to be verified in Razorpay. Only users with Owner or Admin role can access API Keys. See Razorpay’s guide: [API Keys](https://razorpay.com/docs/payments/dashboard/account-settings/api-keys/).
## Step 3: Configure required Razorpay settings for subscriptions
These settings are mandatory when you sell subscriptions:
* **Enable Flash Checkout** in your Razorpay Dashboard. See Razorpay’s docs: [Subscriptions – Flash Checkout](https://razorpay.com/docs/payments/subscriptions/#flash-checkout).
* **Fee bearer model** must be set to **You pay the fees**. The “Convenience fee model” is not supported for subscriptions. See Razorpay’s guide: [Configuration – Fee bearer model](https://razorpay.com/docs/payments/dashboard/account-settings/configuration/#manage-fee-bearer-model).
Without these settings, subscription payments will fail.
## Step 4: Create a webhook in Razorpay
1. In the Razorpay Dashboard, go to **Accounts & Settings → Webhooks**.
2. Click **Add New Webhook**.
3. Set the Webhook URL to your site URL followed by `/webhooks/razorpay`, for example: `https://your-domain.com/webhooks/razorpay`.
4. Select **All events**.
5. Set a **Webhook secret** (any random string). Copy it—you will paste the same value into Aikeedo.
6. Save the webhook.
The webhook secret must match exactly in Razorpay and in Aikeedo.
## Step 5: Enable Razorpay in Aikeedo
Open the Aikeedo admin and go to **Payments → Razorpay**. Fill in the sections as shown in the Aikeedo dashboard screenshot.
### Details section
1. **Status**: Enable the toggle to turn on Razorpay.
2. **Charge Currency**: Choose your billing currency. If you use a currency different from your default currency and have a currency rate provider configured, amounts are converted automatically.
3. **Mode**: Choose **Test** for testing or **Live** for real payments.
For subscriptions, ensure Flash Checkout is enabled and Fee bearer is set to “You pay the fees” in Razorpay, as noted above.
### Keys section
1. **API Key Id**: Paste the value from Razorpay.
2. **API Key Secret**: Paste the secret from Razorpay.
### Webhooks section
1. **Webhook URL**: This is shown in the Aikeedo UI. Create the same URL in Razorpay.
2. **Webhook secret**: Paste the exact secret you set in Razorpay.
3. Click **Save changes**.
Razorpay is now connected. You can accept payments and create subscriptions.
## Testing your integration
1. Set **Mode** to **Test** in Aikeedo and use your Razorpay Test keys.
2. Make a test purchase or subscription using Razorpay’s test methods: [Test Card Details](https://razorpay.com/docs/payments/payments/test-card-details/).
3. Confirm you see payment updates in Aikeedo and in the Razorpay Dashboard.
The test environment does not move real funds. Switch to Live mode only after successful tests.
## Troubleshooting
Check that Flash Checkout is enabled and Fee bearer is set to “You pay the fees” in Razorpay. See: [Subscriptions – Flash Checkout](https://razorpay.com/docs/payments/subscriptions/#flash-checkout) and [Configuration – Fee bearer model](https://razorpay.com/docs/payments/dashboard/account-settings/configuration/#manage-fee-bearer-model).
Verify your site is accessible via HTTPS, the Webhook URL matches `https://your-domain.com/webhooks/razorpay`, and the secret matches in both places.
Re‑copy the API Key Id and API Key Secret from **Accounts & Settings → API Keys** and paste them into Aikeedo.
## Additional resources
* Razorpay Subscriptions – Flash Checkout: [Subscriptions – Flash Checkout](https://razorpay.com/docs/payments/subscriptions/#flash-checkout)
* Fee bearer model configuration: [Configuration – Fee bearer model](https://razorpay.com/docs/payments/dashboard/account-settings/configuration/#manage-fee-bearer-model)
* Test cards and sandbox methods: [Test Card Details](https://razorpay.com/docs/payments/payments/test-card-details/)
# Stripe Integration
Source: https://docs.aikeedo.com/integrations/payment-gateways/stripe
Configure Stripe payment gateway in Aikeedo with API keys, webhooks, and subscription handling for secure online payments and recurring billing.
Stripe integration enables secure payment processing and subscription management for your Aikeedo platform. This guide covers account setup, API configuration, and webhook implementation.
## Prerequisites
Before you begin, ensure you have:
* An active [Stripe account](https://stripe.com/)
* Access to your Aikeedo admin panel
* Your domain properly configured with SSL enabled
## Step 1: Set Up Your Stripe Account
If you haven't already, sign up for a Stripe account:
1. Visit the [Stripe website](https://stripe.com/) and click "Start now" or "Create account"
2. Follow the prompts to create and verify your account
3. Complete any additional verification steps required by Stripe
## Step 2: Obtain Stripe API Keys
To connect Aikeedo with Stripe, you'll need to retrieve your API keys:
1. Log in to your [Stripe Dashboard](https://dashboard.stripe.com/)
2. Navigate to **Developers** > [**API keys**](https://dashboard.stripe.com/apikeys)
3. You'll see two types of keys:
* **Publishable key**: Starts with `pk_`
* **Secret key**: Starts with `sk_`
Keep your secret key confidential! Never share it publicly or include it in
client-side code.
## Step 3: Set Up Stripe Webhooks
Webhooks are crucial for handling recurring payments and keeping your Aikeedo platform in sync with Stripe. Here's how to set them up:
1. In your Stripe Dashboard, go to **Developers** > [**Webhooks**](https://dashboard.stripe.com/webhooks)
2. Click "Add endpoint"
3. Enter your webhook URL: `https://your-aikeedo-domain.com/webhooks/stripe`
4. Under "Events to send," select the following:
* `invoice.created`
* `invoice.updated`
* `invoice.finalized`
* `invoice.marked_uncollectible`
* `invoice.payment_succeeded`
* `invoice.deleted`
* `invoice.voided`
* `customer.subscription.updated`
* `customer.subscription.deleted`
5. Click "Add endpoint" to create the webhook
6. After creation, you'll see a "Signing secret" - copy this for later use
If you're not sure which events to select, it's safe to choose "Select all
events" for comprehensive coverage.
## Step 4: Enable Stripe in Aikeedo
Now that you have your Stripe account set up, it's time to configure Aikeedo:
1. Log in to your Aikeedo admin panel
2. Navigate to **Settings > Payments > Stripe**
3. Toggle the switch to enable Stripe payments
4. Choose the appropriate mode:
* **Test**: For testing payments (use Stripe's test cards)
* **Live**: For processing real transactions
5. Enter the following details:
* **API Publishable Key**: Your Stripe publishable key
* **API Secret Key**: Your Stripe secret key
* **Webhook Secret**: The signing secret from your webhook setup
6. Click "Save changes" to apply your settings
Congratulations! Stripe payments are now enabled on your Aikeedo platform.
## Testing Your Integration
Before going live, it's crucial to test your Stripe integration:
1. Set Aikeedo to "Test" mode
2. Use [Stripe's test card numbers](https://stripe.com/docs/testing) to simulate various payment scenarios
3. Create a test subscription or product purchase
4. Verify that payments are processed correctly and webhooks are functioning
Once you're confident everything is working as expected, switch to "Live" mode to start accepting real payments.
## Troubleshooting
If you encounter any issues with your Stripe integration, try these steps:
1. Double-check all API keys and secrets for accuracy
2. Ensure your webhook URL is correct and accessible
3. Review Stripe's dashboard for any error logs or failed webhook attempts
4. Check Aikeedo's error logs for any integration-related issues
By following this guide, you've successfully integrated Stripe payments into your Aikeedo platform, providing your users with a secure and efficient payment experience.
## Form types
Aikeedo supports three form types for Stripe payments. You can select the form type in **Settings > Payments > Stripe** under the **Form type** dropdown:
| Form type | Description |
| - | - |
| **Simple** | A minimal card-only input form. |
| **Advanced** | A full card form that includes all payment methods enabled in your Stripe account (e.g. wallets, local methods). |
| **Hosted Checkout** | Redirects customers to a Stripe-hosted checkout page to complete payment, rather than an embedded form on your platform. |
## Stripe Automatic Tax Collection
When **Hosted Checkout** is selected as the form type, an additional **Automatic Tax** option becomes available. When enabled, Stripe determines the correct tax rates and collects tax on your behalf — no manual rate configuration or tax engine plugin required.
To enable Stripe Automatic Tax Collection:
1. Navigate to **Settings > Payments > Stripe** in your admin panel.
2. Set **Form type** to **Hosted Checkout**.
3. Under the **Tax** section, toggle on **Automatic Tax**.
4. Click **Save changes**.
Stripe Tax must be activated in your [Stripe
Dashboard](https://dashboard.stripe.com/tax/registrations) before enabling
this option. You should also set a default product tax code in your Stripe Tax
settings as a fallback.
Enable Automatic Tax only when no other tax engine plugin is active in your
billing settings. Running both simultaneously will result in double tax
calculation.
Stripe Automatic Tax Collection is a separate feature from the [Stripe Tax
Engine plugin](/integrations/tax-engines/stripe-tax). The plugin calculates
tax using the Stripe Tax Calculations API for use within Aikeedo's checkout
flow, while Automatic Tax Collection delegates tax handling entirely to
Stripe.
# Xendit
Source: https://docs.aikeedo.com/integrations/payment-gateways/xendit
Configure Xendit as a payment gateway in Aikeedo to accept one-time and recurring payments across Southeast Asia.
The Xendit plugin adds payment acceptance for Southeast Asia to your Aikeedo platform. It supports one-time payment links and recurring subscription plans across six regional currencies, serving customers in Indonesia, the Philippines, Vietnam, Thailand, Singapore, and Malaysia.
Xendit is an additional plugin available for purchase from the [Aikeedo
Marketplace](https://aikeedo.com/marketplace/xendit/?utm_source=docs\&utm_medium=content). Install it from your
admin panel before following this guide.
## Prerequisites
Before you begin, ensure you have:
* A [Xendit](https://www.xendit.co/) business account
* The Xendit plugin installed on your Aikeedo installation
* Access to your Aikeedo admin panel
## Supported currencies
Xendit supports the following Southeast Asian currencies:
| Currency | Country |
| - | - |
| IDR | Indonesia |
| PHP | Philippines |
| VND | Vietnam |
| THB | Thailand |
| SGD | Singapore |
| MYR | Malaysia |
## Step 1: Obtain your API key and webhook token
1. Log in to your [Xendit Dashboard](https://dashboard.xendit.co/).
2. Navigate to **Settings > API Keys**.
3. Copy your **Secret API Key** (starts with `xnd_`).
Keep your secret API key confidential. Never share it publicly or commit it to
version control.
To authenticate incoming webhooks:
1. In the Xendit Dashboard, navigate to **Settings > Webhooks**.
2. Copy the **Webhook Verification Token** shown at the top of the page. Aikeedo uses this token to verify that incoming webhook events are genuine.
## Step 2: Configure the webhook
1. In the Xendit Dashboard, navigate to **Settings > Webhooks**.
2. Under the relevant event types, add your webhook URL:
```
https://your-aikeedo-domain.com/webhooks/xendit
```
3. Enable at minimum the **Subscription** events to keep subscription status in sync.
4. Save the webhook configuration.
## Step 3: Enable Xendit in Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > Payments > Xendit**.
3. Toggle the switch to enable Xendit.
4. Enter your **Secret API Key**.
5. Enter your **Webhook Verification Token**.
6. Click **Save changes**.
Xendit is now enabled on your platform.
## Payment flows
**One-time payments** — Aikeedo creates a Xendit payment session (payment link) and redirects the customer to the Xendit-hosted payment page. After payment, the customer is redirected back to your platform.
**Recurring subscriptions** — Aikeedo creates a Xendit recurring plan and redirects the customer to an authorization page for the initial payment. Subsequent billing cycles are charged automatically. When a subscription is cancelled in Aikeedo, the corresponding Xendit recurring plan is inactivated.
Xendit recurring plans do not support billing dates on days 29–31. If a
subscription would fall on one of those days, Aikeedo automatically shifts the
anchor date to the 1st of the following month.
## Testing
Xendit provides a test mode accessible with test-mode API keys. To test your integration:
1. In the Xendit Dashboard, switch to **Test Mode** and copy your test-mode Secret API Key.
2. Enter the test key in **Settings > Payments > Xendit** in Aikeedo.
3. Use Xendit's test payment methods to simulate one-time and recurring payment scenarios.
4. Verify that webhook events are received and subscription statuses are updated correctly in Aikeedo.
Switch back to your live API key when you are ready to accept real payments.
## Troubleshooting
* **Payments not completing**: Verify that your secret API key is correct and that the account is active in Xendit.
* **Webhooks not received**: Confirm that the webhook URL is publicly accessible and that the correct event types are subscribed in the Xendit Dashboard.
* **Webhook verification failures**: Ensure the **Webhook Verification Token** in Aikeedo matches the token shown in the Xendit Dashboard under **Settings > Webhooks**.
* **Recurring plan errors**: Check that your Xendit account is enabled for recurring payments and that the selected currency is supported in your account region.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# YooKassa Integration
Source: https://docs.aikeedo.com/integrations/payment-gateways/yookassa
Configure YooKassa payment gateway in Aikeedo with API credentials and subscription handling for secure online payments in Russia and CIS countries.
YooKassa integration enables secure payment processing and subscription management for your Aikeedo platform, specifically designed for the Russian and CIS markets. This guide covers account setup and API configuration.
YooKassa integration is available as a separate plugin that must be purchased from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content) before you can configure this payment gateway.
## Prerequisites
Before you begin, ensure you have:
* An active [YooKassa merchant account](https://yookassa.ru/)
* Access to your Aikeedo admin panel
* Your domain properly configured with SSL enabled
* A valid business registration in Russia or CIS countries
## Step 1: Set Up Your YooKassa Account
If you haven't already, sign up for a YooKassa merchant account:
1. Visit the [YooKassa website](https://yookassa.ru/) and click "Подключить" (Connect)
2. Follow the prompts to create and verify your merchant account
3. Complete the business verification process with required documents
4. Wait for account approval (typically 1-3 business days)
YooKassa requires business registration in Russia or CIS countries. Personal accounts are not supported for merchant services.
## Step 2: Obtain YooKassa API Credentials
To connect Aikeedo with YooKassa, you'll need to retrieve your API credentials:
### Get Your Shop ID
1. Log in to your [YooKassa Dashboard](https://yookassa.ru/my)
2. In the left sidebar, find your **Shop ID** under your organization name
* It will show your organization name with "ShopID XXXXXXX"
### Get Your API Secret Key
1. In the left sidebar, click on **Интеграция** (Integration)
2. Select **Ключи API** (API Keys) from the integration submenu
3. You'll find your **Секретный ключ** (Secret Key) in the API Keys section
Keep your Secret Key confidential! Never share it publicly or include it in client-side code. If you lose your key, you can regenerate it using the refresh icon in the dashboard.
## Step 3: Configure YooKassa in Aikeedo
Now that you have your YooKassa account set up, configure the integration in Aikeedo:
1. Log in to your Aikeedo admin panel
2. Navigate to **Payments** in the main menu
3. Click on **YooKassa**
### Details Section
Configure the general settings:
1. **Status**: Toggle the switch to enable YooKassa payments
2. **Charge Currency**: Select your preferred currency
* **Russian Ruble**: Use RUB for all transactions
* **Inherit**: Use your platform's default currency (e.g., USD) without conversion
* If you select a currency different from your default and have a currency rate provider set up, amounts will be automatically converted
3. **VAT code**: Select the appropriate VAT rate for Russian tax compliance
* This information is only for the YooKassa receipt
* Plan prices must be set tax inclusive
4. **Mode**: Choose between:
* **Test**: For testing payments with YooKassa's test environment
* **Live**: For processing real transactions
### Keys Section
Enter your API credentials:
1. **Shop ID**: Enter your YooKassa Shop ID (required)
2. **API secret key**: Enter your YooKassa Secret Key (required)
3. Click **Save changes** to apply your settings
Congratulations! YooKassa payments are now enabled on your Aikeedo platform.
## Testing Your Integration
Before going live, it's crucial to test your YooKassa integration:
1. Set the YooKassa payment gateway to "Test" mode in the Details section
2. Use [YooKassa's test card numbers](https://yookassa.ru/developers/payment-acceptance/testing-and-going-live/testing) to simulate various payment scenarios
3. Create a test subscription or product purchase
4. Verify that payments are processed correctly
YooKassa test environment uses special test card numbers and doesn't process real payments. Always test thoroughly before switching to live mode.
## Troubleshooting
If you encounter any issues with your YooKassa integration, try these steps:
1. **Verify API Credentials**: Double-check your Shop ID and Secret Key in the Keys section
2. **Check Mode Setting**: Ensure you're using Test mode for testing and Live mode for production
3. **Verify Currency**: Make sure your selected currency is supported by YooKassa
4. **Test Payments**: Use YooKassa's test card numbers to verify the integration works correctly
### Common Issues
Double-check your Shop ID and Secret Key. You can find these in your YooKassa Dashboard under Integration > API Keys.
Ensure you're using the correct mode (Test for testing, Live for production) and that your currency is supported.
Make sure you've selected the correct VAT code for your business type and that your plan prices are set tax inclusive.
## Additional Resources
* [YooKassa Developer Documentation](https://yookassa.ru/developers?lang=en)
* [YooKassa API Reference](https://yookassa.ru/developers/api?lang=en#intro)
* [Payment Methods Guide](https://yookassa.ru/developers/payment-acceptance/overview)
* [Testing Guide](https://yookassa.ru/developers/payment-acceptance/testing-and-going-live/testing)
By following this guide, you've successfully integrated YooKassa payments into your Aikeedo platform, providing your users with a secure and convenient payment experience tailored for the Russian and CIS markets.
# Serper API Integration
Source: https://docs.aikeedo.com/integrations/serper
Enhance your Aikeedo platform with Serper API's Google search capabilities. This guide walks you through setting up and leveraging Serper API for real-time search results in your Chat tool.
## Introduction
Aikeedo's Serper API integration unlocks real-time Google search capabilities for your platform. By integrating Serper.dev's API, you can significantly enhance your Chat tool, allowing users to access up-to-date search results directly within their conversations.
## Setting Up the Integration
Follow these steps to connect Serper API with your Aikeedo platform:
### Step 1: Obtain Your Serper API Key
1. If you don't have a Serper account, [sign up here](https://serper.dev/).
2. Once logged in, you'll be taken to your Serper dashboard.
3. In the left sidebar of the dashboard, click on the "API Key" menu item.
4. Copy your API key from the popup that appears—you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to Serper API services and should not be shared publicly.
Serper provides a dashboard where you can monitor your API usage, including the number of requests made in the last 30 days and 24 hours, as well as your total credits used.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > Serper API**.
3. In the "API" section, enter your Serper API key in the "API Key" field.
4. Click **Save changes** to activate the integration.
Great job! Serper API is now integrated with your Aikeedo platform.
## Aikeedo Tools Powered by Serper API
Serper API integration enhances the following Aikeedo tool:
1. **Chat**: Enable real-time Google search results within conversations using the "Google search" capability.
## Best Practices
To make the most of your Serper API integration:
* Use clear and concise search queries for best results.
* Encourage users to specify their search intent when using the Google search capability.
* Monitor your API usage to manage costs effectively.
* Regularly update your integration to access the latest features and improvements.
## Troubleshooting
If you encounter issues with the Serper API integration, try these steps:
1. Verify that your API key is correct and active.
2. Check your Serper account for any usage limits or restrictions.
3. Ensure your Aikeedo platform is up to date.
4. If you receive error messages related to API access or quotas, verify your account status with Serper.
Can't find your API Key? Click the "Click here" link below the API Key field in the Aikeedo settings for additional guidance.
Remember to review Serper's usage policies and pricing to ensure compliance and manage costs effectively as you scale your search integration.
# Stability AI Integration
Source: https://docs.aikeedo.com/integrations/stabilityai
Enhance your Aikeedo platform with Stability AI's advanced image generation capabilities. This guide walks you through setting up and leveraging Stability AI's powerful models for creating unique and high-quality images.
## Introduction
Aikeedo's Stability AI integration unlocks advanced image generation capabilities for your platform. With access to Stability AI's cutting-edge models, you can significantly enhance your Imagine tool, allowing users to create unique and high-quality images from text descriptions.
## Setting Up the Integration
Follow these steps to connect Stability AI with your Aikeedo platform:
### Step 1: Obtain Your Stability AI API Key
1. If you don't have a Stability AI account, [sign up here](https://platform.stability.ai/sign-up).
2. Once logged in, click on your profile image in the top right corner of the page.
3. The API Keys page will open automatically. If not, select "API Keys" from the left side menu.
4. On the API Keys page, you'll see a list of your existing API keys.
5. If you don't have any keys, or want to create a new one, click the "+ Create API Key" button in the top right corner of the API Keys section.
6. Once created, you'll see your new API key in the list. The key itself will be partially hidden for security reasons.
7. Use the copy icon next to your API key to copy it to your clipboard—you'll need this for Aikeedo.
Keep your API key secure and confidential. It grants access to Stability AI services and should not be shared publicly. Stability AI masks part of the key in the dashboard for security reasons.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel.
2. Navigate to **Settings > StabilityAI**.
3. In the "API" section, enter your Stability AI API key in the "API Key" field.
4. Click **Save changes** to activate the integration.
Great job! Stability AI is now integrated with your Aikeedo platform.
## Aikeedo Tools Powered by Stability AI
Stability AI integration enhances the following Aikeedo tool:
1. **Imagine**: Create unique and high-quality images from text descriptions using Stability AI's advanced image generation models.
## Best Practices
To make the most of your Stability AI integration:
* Experiment with different models to find the best fit for your use case.
* Provide clear and detailed text prompts for optimal image generation results.
* Consider the style and complexity requirements of your project when selecting models.
* Use negative prompts to refine and exclude unwanted elements from generated images.
* Monitor your API usage to manage costs effectively.
* Regularly update your integration to access the latest models and features.
## Troubleshooting
If you encounter issues with the Stability AI integration, try these steps:
1. Verify that your API key is correct and active.
2. Check your Stability AI account for any usage limits or restrictions.
3. Ensure your Aikeedo platform is up to date.
4. If you receive error messages related to model access or quotas, verify your account status with Stability AI.
5. For model-specific issues, confirm that your account has access to the required models.
6. If you experience slow response times, consider using a different model or optimizing your prompts.
Regularly review your Stability AI usage and available model options to ensure smooth integration with Aikeedo.
Remember to review Stability AI's usage policies and pricing to ensure compliance and manage costs effectively as you scale your image generation integration.
# Manual Tax Engine
Source: https://docs.aikeedo.com/integrations/tax-engines/manual-tax
Configure flat and percentage tax rates manually in Aikeedo using the Manual Tax Engine plugin, with support for per-country rules and multiple stacked rates.
The Manual Tax Engine plugin lets you define your own tax rates without relying on any external service. You configure a global default rate as a fallback, then add per-country rules with one or more named rates each. Rates can be a percentage of the order amount or a fixed currency amount.
Manual Tax Engine is an additional plugin available for purchase from the
[Aikeedo Marketplace](https://aikeedo.com/marketplace/manual-tax/?utm_source=docs\&utm_medium=content). Install it
from your admin panel before following this guide.
Tax law varies by jurisdiction. Consult a qualified tax professional to
determine the correct rates and rules for your situation. Aikeedo does not
provide tax advice.
## Opening the settings
Navigate to **Settings > Tax Engines** in your admin panel, then click **Manual Tax**.
## Default tax rate
The **Default Tax Rate** section at the top of the page applies a fallback rate to any customer whose billing address does not match a country rule.
* Use the toggle to enable or disable the default rate entirely.
* Enter the rate value in the **Rate** field. The rate is a percentage by default — click the **%** button to switch to a fixed currency amount instead.
## Country rules
The **Rules** section lists per-country tax rules. Each rule overrides the default rate for customers billing from that country and can contain multiple named rates that are added together.
### Adding a rule
1. Click **+ Add rule**.
2. Select a country from the dropdown.
3. If the selected country has subdivisions (states or provinces), an additional **Subdivision** dropdown appears. Selecting a subdivision is optional — leave it blank to apply the rule to the entire country, or select a specific state/province to target that subdivision only.
4. Click **Add rule** to confirm.
The new rule appears in the rules list. Click on it to open the rule and add tax rates.
### Adding rates to a rule
Each country rule can have one or more named rates:
1. Click on a country rule to open it.
2. Click **+ Add rate**.
3. Enter a **name** (shown to the customer, e.g. "VAT") and a **value** (percentage or fixed amount).
4. Repeat for any additional rates.
Multiple rates within a rule are added together. For example, a rule with 18% VAT and 14% Custom Handling Fee results in a combined 32% tax for customers in that country.
### Zero-rating a country
To explicitly exempt a country from tax, add a rule for that country and set the rate value to `0`. This ensures the default rate does not apply to customers from that country.
When a country-level rule (no subdivision selected) exists for a country,
subdivision-level rules for that same country are ignored. Subdivision rules
only apply when there is no country-level rule for that country.
## Saving changes
Click **Save changes** at the bottom of the page to apply your configuration.
## Testing
After configuring your rules, create a test purchase using a billing address that matches each of your configured countries. Verify that the correct tax rates appear in the checkout summary.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Tax Engines
Source: https://docs.aikeedo.com/integrations/tax-engines/overview
Configure tax calculation for your Aikeedo platform using tax engine plugins to collect the correct tax on subscription and one-time purchases.
## Overview
Aikeedo v4.0 introduces a core tax infrastructure layer that enables tax calculation on purchases made through your platform. A tax engine is a plugin that calculates the tax amount on each order based on the customer's billing address and the configured rules or connected tax service.
You can install multiple tax engine plugins and switch between them at any time from your billing settings. Only one engine is active at a time, but all installed engines remain configured and ready to use.
## Available tax engine plugins
Define flat and percentage tax rates manually by country and subdivision. Full control, no external services required.
Automatic tax calculation via the Stripe Tax Calculations API. Requires Stripe to be configured as your payment gateway.
## Selecting the active tax engine
Installing a tax engine plugin does not activate it automatically. You select the active engine from your billing configuration:
1. Navigate to **Settings > Billing** in your admin panel.
2. Under the **Tax** section, open the **Tax engine** dropdown.
3. Select the engine you want to use, or choose **No tax** to disable tax calculation entirely.
4. Click **Save changes**.
The **Tax Engines** page (**Settings > Tax Engines**) is where you configure each individual engine's rules and settings independently of which one is currently active.
## Tax base
In **Settings > Billing > Tax**, you can also set the **Tax base** — the amount that tax is calculated against:
| Option | Description |
| - | - |
| **Net after coupon** | Tax is calculated on the discounted amount after any coupon is applied. If no coupon applies, the full plan list price is used as the base. |
| **Full plan list price** | Tax is always calculated on the full plan price, regardless of any discounts. |
## Stripe Automatic Tax Collection
If you use Stripe as your payment gateway with the **Hosted Checkout** form type, there is an additional option that does not require a tax engine plugin: [Stripe Automatic Tax Collection](/integrations/payment-gateways/stripe#stripe-automatic-tax-collection).
With this feature enabled, Stripe handles both tax calculation and collection end-to-end within its hosted checkout flow. This only applies to payments processed through Stripe — other payment gateways are unaffected. It is also distinct from the Stripe Tax Engine plugin, which calculates tax for non-hosted Stripe checkout flows.
## How tax is applied
1. A customer proceeds to checkout with a billing address on file.
2. Aikeedo calls the active tax engine with the order amount and billing address.
3. The tax engine returns one or more tax lines (name, rate, amount).
4. The tax lines are displayed to the customer and included in the total.
5. The payment gateway collects the full amount, including tax.
Tax is calculated based on the customer's **workspace billing address**.
Customers must have a billing address on file for tax to be applied. If no
address is present, no tax is charged.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Stripe Tax Engine
Source: https://docs.aikeedo.com/integrations/tax-engines/stripe-tax
Configure automatic tax calculation in Aikeedo using the Stripe Tax Engine plugin, powered by the Stripe Tax Calculations API.
The Stripe Tax Engine plugin integrates Aikeedo's billing system with the [Stripe Tax Calculations API](https://stripe.com/docs/tax). When active, it automatically determines the correct tax amount for each purchase based on the customer's billing address and your Stripe Tax configuration — without requiring you to manually define rates or rules.
Stripe Tax Engine is an additional plugin available for purchase from the
[Aikeedo Marketplace](https://aikeedo.com/marketplace/stripe-tax/?utm_source=docs\&utm_medium=content). Install it
from your admin panel before following this guide.
This plugin calculates tax for standard (non-hosted) Stripe checkout flows. If
you use [Stripe Hosted
Checkout](/integrations/payment-gateways/stripe#form-types), consider using
[Stripe Automatic Tax
Collection](/integrations/payment-gateways/stripe#stripe-automatic-tax-collection)
instead, which handles tax entirely within the Stripe-hosted flow.
## Prerequisites
The settings page includes a built-in checklist that shows whether all prerequisites are met:
* **Stripe integration enabled** — Stripe must be enabled in your payment settings. Tax is only calculated while Stripe is active; if Stripe is disabled, no tax lines are added.
* **Secret key configured** — your Stripe secret key must be set in the Stripe payment settings.
* **Stripe Tax active on account** — Stripe Tax must be activated in your [Stripe Dashboard](https://dashboard.stripe.com/tax/registrations), with registrations for the jurisdictions where you have tax obligations.
Tax will only be calculated for jurisdictions where you have an active
registration in your Stripe account. Review [Stripe's Tax
documentation](https://stripe.com/docs/tax) to understand what registrations
are required for your business.
## Configuration
Navigate to **Settings > Tax Engines > Stripe Tax** in your admin panel.
### Product tax code (optional)
A Stripe product tax code tells Stripe what you sell (for example, SaaS vs. physical goods) so the correct tax rules apply. Enter the code in the **Tax code** field (format: `txcd_...`).
If left blank, Stripe uses the default tax code configured in your Stripe Dashboard Tax settings.
To find the right code:
* [Stripe Tax: product tax categories](https://stripe.com/docs/tax/tax-categories) — lists common codes for software, SaaS, and other product types.
* [Stripe Dashboard: Tax settings](https://dashboard.stripe.com/settings/tax) — set your default product tax code and manage Stripe Tax for your account.
### API keys
Stripe Tax uses the same API keys as your Stripe Payments integration. No separate key configuration is required on this page.
## Activating the plugin
Once configured, select **Stripe Tax** as the active tax engine in **Settings > Billing Configuration > Tax engine**. See the [Tax Engines overview](/integrations/tax-engines/overview#selecting-the-active-tax-engine) for details.
## Troubleshooting
* **Checklist items not passing**: Address each failing item — ensure Stripe is enabled in Payments, the secret key is set, and Stripe Tax is active on your Stripe account.
* **Tax not appearing at checkout**: Confirm that the customer has a billing address on file and that you have active Stripe Tax registrations for their jurisdiction.
* **Incorrect tax amounts**: Review your Stripe Tax registrations and product tax code to ensure they match your product category and the customer's jurisdiction.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# xAI Integration
Source: https://docs.aikeedo.com/integrations/xai
Enhance your Aikeedo platform with xAI's Grok models. This guide explains how to set up and configure xAI integration for your platform.
## Introduction
Aikeedo's xAI integration allows you to leverage Grok models for AI-powered conversations and content generation. This guide will help you set up and manage the xAI integration in your platform.
## Setting Up the Integration
Follow these steps to connect xAI with your Aikeedo platform:
### Step 1: Obtain Your xAI API Key
1. Sign up for xAI API access at [console.x.ai](https://console.x.ai/)
2. Navigate to API Keys page
3. Generate a new API key by clicking **Create API Key**
4. Copy the generated API key for use in Aikeedo
Store your API key securely and never share it publicly. The key provides direct access to xAI services.
### Step 2: Configure Aikeedo
1. Log in to your Aikeedo admin panel
2. Navigate to **Settings > xAI / Grok**
3. Paste your xAI API key into the API Key field
4. Click **Save Changes** to activate the integration
Once saved, xAI services will be available across your Aikeedo platform.
## Aikeedo Tools Using xAI
The xAI integration powers these Aikeedo tools:
1. **Chat**: Engage in conversations using Grok's capabilities
2. **Writer**: Answer a template's questions and continue the result in chat
3. **Imagine**: Create images with Grok Image models
4. **Video**: Generate videos with Grok Video models
## Best Practices
To optimize your xAI integration:
* Monitor your API usage regularly
* Keep your API key secure and up to date
* Test the integration after setup to ensure everything works correctly
## Troubleshooting
If you encounter issues:
1. Verify your API key is correct and active
2. Check if your xAI account has active subscription
3. Ensure your Aikeedo platform is updated to the latest version
4. Verify your account has access to the required models
If you receive authentication errors, double-check your API key and ensure your xAI subscription is active.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Commercial License Resources
Source: https://docs.aikeedo.com/overview/commercial-resources
Access exclusive resources for Extended/Commercial License users, including Figma designs and pricing calculators.
Here, you'll find valuable tools and assets exclusively available to our Extended/Commercial License users. These resources are designed to enhance your experience and help you make the most of Aikeedo's powerful AI-driven content creation capabilities.
## Why are these resources exclusive?
These supplementary resources are offered as a complimentary benefit to our Extended/Commercial License holders. Here's why:
1. **SaaS Focus**: These tools are primarily useful if you're using Aikeedo as a SaaS (Software as a Service) platform and plan to charge end users for access.
2. **Added Value**: While not essential for the core functionality of Aikeedo, these resources provide additional support for business-oriented implementations.
3. **Specialized Use**: The Figma designs and pricing calculator are tailored for users who are looking to customize and monetize their Aikeedo instance.
These resources do not affect any functionality within the Aikeedo app itself. They are provided as complementary tools to assist Extended/Commercial License users in maximizing the potential of their Aikeedo implementation.
## Accessing Commercial Resources
Our commercial resources, including the Figma design file and the Aikeedo Price Calculator, are stored in a single, secure folder. Here's how to gain access:
1. **Important:** Have your Extended/Commercial License key (purchase code) ready before proceeding. Requests without a valid license code will not be processed.
2. Visit the [Aikeedo Commercial Resources](https://drive.google.com/drive/folders/1AHqGqHT6tQyRwRxS2v2j0RngxGbqr_pA?usp=drive_link) link.
3. In the "Message (optional)" text area, enter your Extended/Commercial License key.
4. Click the "Request access" button to submit your access request.
5. Our team will verify your license and grant you access to the folder containing all commercial resources.
These resources are exclusive benefits for Extended/Commercial License users and are not included in the standard Aikeedo package. Always include your license code when requesting access to ensure prompt processing of your request.
## Available Resources
### 1. Figma Design File
Get access to our professionally crafted Figma design file for the Aikeedo landing page. This resource is perfect for customizing your Aikeedo instance or drawing inspiration for your own AI-powered projects.
### 2. Aikeedo Price Calculator
Our custom-built price calculator helps you determine optimal pricing strategies for your Aikeedo-powered services. This tool is invaluable for setting up your credit system and pricing plans.
## Using the Price Calculator
Our price calculator is designed to be user-friendly and intuitive, even for those without a technical background. Here's a quick guide to get you started:
1. **Input Fields**: Look for green cells in the spreadsheet. These are where you'll enter your data.
* Define the total number of credits you want to sell per 1 USD.
* Set your desired profit margin.
2. **Output Fields**: Orange cells display the calculated results.
* These show the credit conversion ratios you'll use in the Aikeedo Admin UI.
3. **Pricing Plans**: Scroll down to find sections where you can define pricing plans in your preferred currency.
* The calculator will automatically determine the maximum credits for each plan based on your defined profit margin.
Hover over cells with small black triangles in the corner for additional guidance and explanations.
## Best Practices
* **Experiment with Different Scenarios**: Use the calculator to model various pricing strategies before implementing them in your Aikeedo instance.
* **Regular Reviews**: Periodically revisit your pricing structure to ensure it aligns with your business goals and market conditions.
* **Combine with Analytics**: Use the insights from the price calculator in conjunction with your user analytics for a comprehensive pricing strategy.
* **Customize Your Design**: Leverage the Figma file to create a unique look for your Aikeedo instance that aligns with your brand identity.
Thank you for choosing Aikeedo! We're excited to see how you'll leverage these resources to create an outstanding AI-powered content creation platform.
# File Structure
Source: https://docs.aikeedo.com/overview/file-structure
Understand the Aikeedo package structure, including server files, documentation, and support resources to prepare for installation.
Before you begin, make sure you've completed your [Aikeedo purchase](https://aikeedo.com?utm_source=docs). If you haven't yet, head over to our website to get started!
## Overview
After purchasing Aikeedo, you'll receive a ZIP archive containing all the necessary files to set up your AI-powered platform. This guide will walk you through the contents of your download and explain the purpose of each component.
## Unpacking Your Aikeedo Files
Follow these simple steps to access your Aikeedo files:
1. **Download the ZIP archive**: After your purchase, you'll receive a link to download the Aikeedo package.
2. **Locate the download**: Find the ZIP file in your computer's download folder or the location you specified.
3. **Extract the contents**: Use your computer's built-in unzip tool or a third-party application like 7-Zip to extract the files.
If you're unsure how to extract ZIP files on your operating system, a quick web search for "how to unzip files on \[Your OS]" will provide step-by-step instructions.
## Understanding the File Structure
Once you've extracted the ZIP archive, you'll find the following components:
### 1. documentation.pdf
This PDF file contains a direct link to the comprehensive online documentation you're currently reading. It's a quick way to access the most up-to-date information about Aikeedo.
Pro tip: Bookmark the documentation link for easy access in the future!
### 2. support.pdf
This PDF contains information about our support options and how to access paid support services if you need assistance with Aikeedo.
For technical support and assistance, please refer to the support information in this PDF for available support packages and contact details.
### 3. aikeedo-server-files.zip
This is the heart of your Aikeedo package. This ZIP file contains all the core files required to set up and run Aikeedo on your server. Here's what you need to know about it:
* It includes the main application files, configuration templates, and any necessary scripts.
* You'll need to extract and upload these files to your server during the installation process.
* Detailed instructions for handling these files can be found in our [Installation Guide](/setup/installation).
Always keep a backup of the `aikeedo-server-files.zip` in a safe place. You may need it for future updates or if you decide to move your installation to a different server.
## Next Steps
Now that you understand the contents of your Aikeedo download, you're ready to move forward with the installation process. Here's what to do next:
Ensure your hosting environment is compatible with Aikeedo
Follow step-by-step instructions to set up Aikeedo on your server
Configure your Aikeedo instance with technical settings
By following this guide and the subsequent installation steps, you'll be well on your way to launching your own AI-powered platform with Aikeedo. Good luck, and welcome to the Aikeedo community!
# Get Aikeedo
Source: https://docs.aikeedo.com/overview/get-aikeedo
Purchase Aikeedo license from official channels, download the software package, and access your license key for installation.
This guide covers the Aikeedo purchase process, what you receive after purchase, and how to access your installation files.
## Purchasing Aikeedo
Aikeedo licenses are available through our official website:
Purchase directly from our official website to stay up-to-date with the latest offers and updates.
## What You'll Receive
Upon completing your purchase, you'll gain access to:
1. **Aikeedo Software Package**: A downloadable ZIP file containing the complete Aikeedo source code, ready for installation.
2. **License Key**: A unique activation key that you'll use to validate your Aikeedo installation on your server.
These resources empower you to deploy and customize Aikeedo to meet your specific requirements.
Keep your license key secure and confidential. It's tied to your purchase and is required for installation and updates.
## Understanding Your License
Your Aikeedo license grants you the right to use the software according to the terms outlined in our License Agreement. Here are some key points to remember:
* Each license is valid for a single installation (one domain).
* You can create derivative works and customize Aikeedo for your needs.
* Redistribution or resale of Aikeedo is not permitted.
For a complete understanding of your rights and obligations, please review our [License Agreement](https://aikeedo.com/license/?utm_source=docs\&utm_medium=content).
## Accessing Your Purchase
After your purchase is complete, you'll receive an email with instructions on how to download your Aikeedo package. Here's what to do next:
1. **Check your email**: Look for a message from Aikeedo (via LemonSqueezy).
2. **Download the ZIP file**: Follow the provided link to download the Aikeedo software package.
3. **Locate your license key**: Your unique license key will be included in the email or your account dashboard.
## Next Steps
With your Aikeedo package ready, proceed to installation:
Review the package contents and directory organization
Verify your hosting environment meets all compatibility requirements
Follow step-by-step instructions to install Aikeedo on your server
Complete technical configuration including cron jobs
Configure all application settings and features
You now have everything needed to begin installing Aikeedo.
# Introduction to Aikeedo
Source: https://docs.aikeedo.com/overview/introduction
Discover how to launch your AI SaaS business with Aikeedo - a self-hosted, source-available PHP platform for AI-powered content generation and monetization.
## Getting started
Aikeedo is a ready-made PHP software designed to help you launch your AI SaaS business quickly and efficiently. Whether you're new to AI services or looking to expand your offerings, Aikeedo provides the tools and flexibility you need to get started and grow your business.
A step by step guide to installing Aikeedo on your server
Explore prompt templating syntax and how to use it
Explore development guide to customize Aikeedo to your needs
Explore API basics and overview for integrating Aikeedo with your applications
## Your AI SaaS business, ready by this week!
Aikeedo is a powerful, ready-made PHP software designed to launch your AI SaaS business quickly and generate revenue fast. With its customizable features, Aikeedo stands out as a **self-hosted**, **source-available**, and **standalone SaaS-ready** PHP web application.
* **Self-Hosted**: Take full control of your data and hosting environment. Enjoy the flexibility of managing your platform on your own terms.
* **Source-Available**: We provide the full, non-obfuscated source code with your license purchase. This allows you to explore, customize, and tailor the software to your specific needs, giving you complete control over your AI SaaS platform.
* **Standalone SaaS Ready**: Aikeedo is not just a web application; it's a complete Software as a Service (SaaS) solution. Deploy seamlessly and experience the benefits of a robust content platform.
With Aikeedo, you can have your AI SaaS business up and running in a week, giving you a head start in the competitive AI market.
## Connect with @heyaikeedo
Stay updated and connect with us on social media! Follow us [(@heyaikeedo)](https://twitter.com/heyaikeedo) for the latest news, updates, and community discussions. Join the conversation, share your experiences, and be part of the Aikeedo community.
Explore more sections of our documentation to dive deeper into the features, installation guides, and best practices. Let's embark on this exciting journey together!
# Server Requirements
Source: https://docs.aikeedo.com/overview/server-requirements
Complete server requirements for Aikeedo installation including PHP 8.2+, MySQL 8.0+, required extensions, and recommended hosting specifications.
This guide outlines the necessary components and configurations to ensure your server is ready to host Aikeedo efficiently.
## Core Requirements
### PHP and MySQL Versions
Aikeedo requires up-to-date versions of PHP and MySQL to ensure optimal performance and security:
8.2 or later
8.0 or later
Using the latest stable versions is recommended for the best experience and to take advantage of the newest features and security updates.
### Essential PHP Extensions
To run Aikeedo smoothly, make sure your PHP installation includes these extensions:
* ctype
* curl
* dom
* fileinfo
* intl
* json
* libxml
* mbstring
* openssl
* pcre
* phar
* simplexml
* tokenizer
* xml
* xmlwriter
* zip
Most of these extensions are typically included in standard PHP installations. If you're unsure, consult your hosting provider or server administrator.
## Minimum Server Requirements
To ensure optimal performance of Aikeedo, we recommend the following minimum server specifications:
2 GB RAM or more
2 vCPUs or more
For our demo environment, we use a VPS with 4 GB Memory and 2 Intel vCPUs on DigitalOcean. This setup provides excellent performance for most use cases.
### Hosting Recommendations
While Aikeedo can run on various hosting environments, we strongly recommend using a Virtual Private Server (VPS) for better control and performance, especially for commercial use cases.
Start with a smaller VPS configuration and resize as needed. This approach allows you to scale your resources based on your actual usage and growth.
Popular VPS providers that work well with Aikeedo include:
* DigitalOcean (Use our [referral link](https://m.do.co/c/084136521a6a) for \$200 in credits, valid for 60 days)
* Vultr (Use our [referral link](https://www.vultr.com/?ref=9690154) for \$300 in credits, valid for 30 days)
* Amazon Web Services ([AWS](https://aws.amazon.com/))
While most shared hosting environments are supported, they may have limitations that could affect Aikeedo's performance. For the best experience, especially for commercial use, we strongly advise using a VPS.
Special note about Cloudways: The text streaming function doesn't work on Cloudways servers due to their complicated server stack. Unfortunately, they don't provide root access to the server to modify the configuration. This issue cannot be resolved at the application level as it's not application-related. If text streaming is crucial for your use case, we recommend choosing a different hosting provider.
## PHP Configuration
Proper PHP configuration is crucial for Aikeedo to function correctly. Adjust the following settings in your `php.ini` file:
* **File Uploads:** On
* **Post Max Size:** 25MB or more
* **Upload Max Filesize:** 25MB or more
If you're not familiar with editing the `php.ini` file, don't worry! Many hosting control panels provide user-friendly interfaces to adjust these settings. If you need help, check out the [PHP configuration guide](https://www.php.net/manual/en/configuration.file.php) or contact your hosting provider.
## Web Server Compatibility
Aikeedo is designed to work with popular web servers, ensuring flexibility in your hosting setup:
Fully tested and supported
Fully tested and supported
While Apache and Nginx are our primary supported web servers, Aikeedo is built to be compatible with most standard web server configurations.
New to web server setup? Check out these helpful guides:
* [How to install Apache](https://httpd.apache.org/docs/2.4/install.html)
* [How to install Nginx](https://docs.nginx.com/nginx/admin-guide/installing-nginx/installing-nginx-open-source/)
## Next Steps
Once you've confirmed that your server meets these requirements, you're ready to move on to the installation process!
Learn how to install Aikeedo on your server
# Chatbots
Source: https://docs.aikeedo.com/plugins/chatbots
Install and configure the Chatbots plugin to let your subscribers build and embed AI-powered chatbots on their own websites.
The Chatbots plugin is currently **experimental**. It is fully functional, but
the API and admin configuration options may change in future releases.
The Chatbots plugin adds a chatbot builder to your Aikeedo platform. Once installed, your subscribers can create AI-powered chatbots from within their workspace, train them on custom knowledge base sources, and embed the widget on their own websites. All conversations and contacts from the embedded widget are tracked in the subscriber's workspace inbox.
## Prerequisites
* Aikeedo v5.0.0 or later for the latest version of the plugin (earlier plugin versions support v4.x)
* At least one AI provider configured (OpenAI, Anthropic, Cohere, xAI, Ollama, or a custom OpenAI-compatible provider)
* Access to your Aikeedo admin panel
## Installation
The Chatbots plugin is available for purchase from the [Aikeedo Marketplace](https://aikeedo.com/marketplace/chatbots/?utm_source=docs\&utm_medium=content).
1. Purchase and download the plugin package.
2. In your admin panel, navigate to **Plugins** and upload the package.
3. Click **Install** and follow the on-screen instructions.
Once installed, the Chatbots, Sources, Inbox, and Contacts sections become
available in your subscribers' workspaces.
## Admin settings
### Platform-level settings
Navigate to **Settings > Features > Chatbots** in your admin panel to configure platform-wide settings.
**General**
* **Status** — enable or disable the Chatbots feature across your entire platform.
**Powered by**
Controls the branding shown at the bottom of all embedded chatbot widgets:
* **Icon** — upload your brand icon (PNG, SVG, or WEBP, recommended 64×64px).
* **Brand name to display** — the name shown next to the "Powered by" label. Leave empty to hide the branding section entirely.
* **Link to the brand** — the URL the branding label links to.
**Instruction Templates**
Predefined system prompts that subscribers can select when configuring their chatbot agent:
* **Enable instruction templates** — when enabled, subscribers see a template selector when setting up their chatbot's agent instructions.
* **Manage Instructions** — opens the instruction templates editor where you can create, edit, and import built-in prompt templates. A set of ready-to-use templates (e.g. "Base instructions", "Customer support agent") can be imported with one click.
### Plan-level settings
Per-plan chatbot controls are in the **Extensions** tab of each plan's configuration (**Plans > \[plan name] > Extensions > Chatbots**):
* **Allow Branding Removal** — when enabled, subscribers on this plan can hide the "Powered by" text from their chatbot widget.
* **Chatbot Limit** — maximum number of chatbots a subscriber on this plan can create. Leave blank to allow unlimited chatbots.
## Supported AI providers
The Chatbots plugin works with all AI providers already configured on your platform:
* OpenAI
* Anthropic
* Cohere
* xAI (Grok)
* Ollama
* Any custom OpenAI-compatible provider
Subscribers choose which model powers their chatbot from the models available on their plan.
## Subscriber experience (overview)
The chatbot builder is subscriber-operated, but understanding its structure helps you support your users.
### Creating a chatbot
Subscribers navigate to **Chatbots** in their workspace sidebar and click **+ Add new**. They enter a chatbot name (internal only — not visible to end users) to create it.
### Chatbot editor
Each chatbot has a left-sidebar editor with the following sections:
**General**
* **Name** — internal identifier, not shown to end users.
* **Access** — either **Only me** (only the owner can configure it) or **Workspace** (all workspace members can configure it). Conversations, contacts, and inbox are always shared with all workspace members.
* **Status** — toggle between Published and unpublished.
**Agent**
* Configure the AI model, instructions (system prompt), and select an instruction template if enabled.
**Train**
* Add knowledge base sources to ground the chatbot's responses. Sources can be added as:
* **Select sources** — pick from existing Sources in the workspace
* **Add files** — upload documents or PDFs directly
* **Add links** — add URLs to crawl
**Widget > Content**
* Configure the three widget spaces:
* **Home** — landing screen with welcome message, quick-action buttons, and links
* **Messages** — the main conversation interface
* **Help** — FAQ links and a help center button
**Widget > Style**
* **Color scheme** — Light or Dark
* **Position** — Right or Left
* **Chat icon** — custom launcher icon (PNG, SVG, WEBP up to 1MB)
* **Colors** — primary color and text color overrides
**Widget > Security**
* **Domain restrictions** — restrict the widget to specific trusted domains. Supports wildcards (e.g. `*.example.com`). Note that `*.example.com` does not include the apex domain `example.com` — add it separately if needed. Leave the list empty to allow the widget on any domain.
* **Identity verification** — generate a JWT secret key to verify logged-in end-users. Enables personalized, secure interactions.
**Widget > Deploy**
* Copy the JavaScript embed snippet and paste it into any website's HTML to install the widget.
**Engagement > Conversations**
* All conversations initiated through the embedded widget, with full message history and contact details.
**Engagement > Contacts**
* All contacts captured through the widget, including first seen date, location, IP, and conversation history.
## Troubleshooting
* **Chatbots section not appearing after installation**: Clear the admin cache via **Settings > Advanced > Clear Cache** and reload.
* **Chatbot not responding**: Verify that at least one AI provider is configured and that the subscriber's plan grants access to at least one model.
* **Widget not loading on a website**: Check that the website's domain is listed under trusted domains in the chatbot's Security settings, or that domain restriction is disabled.
* **"Powered by" still visible after plan allows removal**: Confirm that **Allow Branding Removal** is enabled on the subscriber's plan, and that the subscriber has toggled it off in their widget settings.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Application Configuration
Source: https://docs.aikeedo.com/setup/configuration
Complete guide to configuring your Aikeedo application settings after installation.
After completing the initial technical setup, you'll need to configure your Aikeedo application to match your business requirements. This guide provides a comprehensive checklist of all settings you should review and configure.
You can configure these settings in any order, but we recommend following this checklist to ensure you don't miss any important configurations.
## Essential Settings Checklist
### General Settings
Configure your site's core details including website name, domain settings, SSL configuration, landing page, default app page, and SEO settings.
Complete step-by-step instructions for configuring all general settings including domain, SSL, and SEO optimization.
### Public Details
Configure your business information and social media presence at **Settings > Public Details**:
**Business Details:**
* **Business Name**: Your company or organization name
* **Business Address**: Complete business address for legal and contact purposes
**Social Media Accounts:**
* X (Twitter), Facebook, Instagram, TikTok
* Discord, LinkedIn, YouTube, GitHub
* Telegram, VK (VKontakte)
Social media links are optional but help establish your brand presence and provide additional ways for users to connect with your business.
These details appear in your site footer and contact pages, building trust and credibility with your users.
### Legal Policies
Configure your legal documents at **Settings > Policies**:
* **Terms of services**: Define usage terms and conditions for your platform
* **Privacy policy**: Explain how you collect, use, and protect user data
* **Refund policy**: Set clear expectations for refunds and returns
**Important Features:**
* **Markdown support**: Format your policies using Markdown syntax for better readability
* **External redirection**: Include full URLs in content to redirect users to external policy pages
Ensure your policies comply with applicable laws and regulations in your jurisdiction. Consider consulting with a legal professional if needed.
### Features & Models
Go to **Settings > Features** to control features available to your users. Disable features you don't plan to offer to keep your interface clean and focused.
A few feature settings worth reviewing:
* **Chat → Capabilities**: turn on **Media generation** (images, video, voice-overs and transcripts from chat), **Canvas** (documents in a side panel) and **Clarifying questions** (the model asks multiple-choice questions before answering an ambiguous request). Each capability must also be enabled on your plans.
* **Chat → Connectors**: lets users connect MCP servers, such as Notion or Linear, and use their tools in chat. See [Connectors](/advanced/connectors).
* **Privacy & safety**: turns on **Private mode**, which makes everything a user creates temporary until they turn the mode off, and sets how many days private items are kept. See [Private Mode](/advanced/private-mode).
* **Writer**: turns on the template wizard, which asks a template's questions and continues in chat. Manage the templates under **Templates**. See [Prompt Templates](/advanced/prompt-templates).
* **Community**: lets users share Imagine and Video generations to a feed visible to everyone on your installation. **Minimum shared generations** keeps the Community tab hidden until a tool has that many shared generations.
* **Utilities**: turns **Prompt enhancement** and **Surprise me** on or off for the Imagine and Video composers, and sets the model they use. It also sets the **Title generator** model for the whole installation.
To import the templates that ship with Aikeedo, go to **Settings > Starter content**. See [Starter Content](/advanced/seed-data). To choose which languages your users can pick, go to **Settings > Languages**. See [Localization](/advanced/localization).
Navigate to **Settings > Models** to activate specific AI models.
Models must be enabled globally here before they can be used. You'll also need to configure the corresponding AI provider integrations.
### Branding
Customize your platform's look and feel at **Settings > Branding**.
For detailed instructions, see our [Branding Guide](/website-basics/branding).
### Billing & Payments
#### Credit System
Configure credit ratios at **Settings > Credit Ratios**:
* Set how credits convert to different AI operations
* Define credit costs for different models
* Adjust ratios based on your pricing strategy
Learn more in our [Unified Credit System](/billing/unified-credit-system) documentation.
#### Plans & Subscriptions
Set up your subscription plans at **Settings > Billing**:
* Create subscription tiers (Free, Pro, Enterprise, etc.)
* Define credit allocations per plan
* Set pricing for each tier
For comprehensive guidance, see our [Billing Overview](/billing/overview) and [Plans, Snapshots & Subscriptions](/billing/plans-snapshots-subscriptions) guides.
#### Payment Gateways
Configure payment processors at **Settings > Payments**:
Set up Stripe for card payments and subscriptions
Configure PayPal for alternative payment options
#### Exchange Rates
Set up currency conversion at **Settings > Exchange**. See our [Currency API Integration](/integrations/currency-api) guide for details.
### Email Configuration
Configure email delivery at **Settings > Email**:
* Choose your email delivery method (SMTP, API, etc.)
* Configure SMTP server settings
* Test email delivery
Follow our comprehensive email setup guide
Also see: [SMTP Configuration](/email/smtp) for detailed SMTP setup instructions.
### Integrations
Connect third-party services at **Settings > Integrations**:
#### AI Provider Integrations
* [OpenAI](/integrations/openai) - GPT models
* [Anthropic Claude](/integrations/claude) - Claude models
* [Cohere](/integrations/cohere) - Cohere models
* [xAI](/integrations/xai) - Grok models
* [Custom LLM Servers](/integrations/ollama) - Self-hosted models
* [Stability AI](/integrations/stabilityai) - Stable Diffusion
* [Clipdrop](/integrations/clipdrop) - AI image tools
* [Fal.ai](/integrations/falai) - Fast AI generation
* [ElevenLabs](/integrations/elevenlabs) - Voice synthesis
* [Google Cloud Platform](/integrations/gcp) - Speech services
* [Azure](/integrations/azure) - Microsoft speech services
#### Other Integrations
* [Google Analytics](/integrations/google-analytics) - Track site usage
* [Google Tag Manager](/integrations/google-tag-manager) - Manage tracking tags
* [OneSignal](/integrations/onesignal) - Push notifications
* [Intercom](/integrations/intercom) - Customer support chat
* [Custom Scripts](/integrations/custom-scripts) - Add custom tracking codes
Start with one or two AI providers to test your setup, then expand to additional providers based on user demand and your business needs.
### Authentication
Configure user authentication options at **Settings > Authentication**:
* [Account Settings](/authentication/accounts-settings) - Configure registration and login
* [Social Login Providers](/authentication/identity-providers/google) - Add Google, Facebook, GitHub, LinkedIn
* [CAPTCHA](/authentication/captcha) - Prevent spam and bot registrations
## Verification Checklist
Before launching your platform to users, verify you've completed:
* [ ] General settings configured (site name, domain, SEO)
* [ ] Public details added (company info, social links)
* [ ] Legal policies published (Terms, Privacy)
* [ ] Features enabled for your target audience
* [ ] AI models activated and tested
* [ ] Subscription plans created and priced
* [ ] Payment gateways configured and tested
* [ ] At least one AI provider configured
* [ ] Email delivery configured and tested
* [ ] Analytics tracking set up (optional)
* [ ] Create a test user account
* [ ] Test subscription purchase flow
* [ ] Verify AI features work correctly
* [ ] Confirm email delivery
## Next Steps
After completing your application configuration:
1. **Test Everything**: Create test accounts, make test purchases, and verify all features work as expected
2. **Customize Branding**: Review our [General Settings](/website-basics/essentials) and [Branding](/website-basics/branding) guides
3. **Advanced Configuration**: Explore [Advanced Topics](/advanced/localization) like localization and email templates
4. **Launch**: Open your platform to real users and start growing your business!
Congratulations! Your Aikeedo platform is now fully configured and ready to serve users.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Initial Setup
Source: https://docs.aikeedo.com/setup/initial-setup
Configure cron jobs, set your site domain, and enable SSL for your Aikeedo installation to ensure proper functionality and security.
After successfully installing Aikeedo, complete these crucial technical setup steps to ensure your application runs smoothly and securely.
## Setting Up the Cron Job
A cron job is essential for automating recurring tasks in Aikeedo, particularly for renewing subscription usages.
It also keeps [connectors](/advanced/connectors) up to date, refreshing each connection's list of tools every few hours.
### Frequency and Interval
* **Renewal Period:** Subscription usages are renewed every 30 days.
* **Recommended Interval:** The cron task should run once per minute for optimal performance.
Some servers may limit cron tasks to a minimum interval of 15 minutes. In such cases, subscriptions will still renew, but there might be a slight delay of up to 15 minutes.
### Configuring the Cron Task
It is important that the cron job runs as the same user that your web server uses to serve your Aikeedo application. On many Linux systems, this user is typically `www-data` (for servers running Apache or Nginx), but it may differ depending on your server setup (for example, `apache`, `nginx`, or another user). Running the cron job as the correct user ensures that file permissions and environment variables are consistent with your web server, preventing potential permission issues.
#### How to Set Up the Cron Job for the Web Server User
1. **Determine the Web Server User:**
* On most systems, the user is `www-data`.
* You can check the running user with:
```shell theme={null}
ps aux | egrep '(apache|nginx|httpd)'
```
* Look for the user column in the output.
2. **Edit the Crontab for the Web Server User:**\
Use the following command, replacing `www-data` with your web server user if different:
```shell theme={null}
sudo crontab -u www-data -e
```
This opens the crontab editor for the specified user.
3. **Add the Cron Entry:**\
Add the following line to the crontab:
```shell theme={null}
* * * * * /usr/bin/php /path/to/app/root/cron.php
```
Replace `/path/to/app/root/` with the actual path to your Aikeedo application's root directory.
If your server uses a different user for the web server (such as `apache` or `nginx`), substitute `www-data` with the appropriate username in the `sudo crontab -u USERNAME -e` command.
#### Important: File Permissions and Cache Issues
While it is technically possible to run the cron job as `root` or another user, this is **not recommended**. If the cron job runs as a different user than your web server (for example, as `root`), the application may create new files in the app cache with incorrect ownership. This can cause issues when you try to clear the cache from the Aikeedo admin panel (Status page).
* **How to Detect the Issue:**\
When you clear the cache from the Status page, you should see a success message in the toast notification. If you do **not** see a success message, it likely means the app is unable to clear the cache due to file permission issues.
* **How to Fix:**\
To resolve this, reset the ownership of your app files to the web server user (e.g., `www-data`) by running:
```shell theme={null}
sudo chown -R www-data:www-data /path/to/app/root
```
Replace `/path/to/app/root` with the actual path to your Aikeedo application's root directory.
After fixing the permissions, ensure the cron job is running as the correct user to prevent this issue from recurring.
Always run the cron job as the same user that your web server uses (typically `www-data`, `apache`, or `nginx`). This ensures proper file permissions and smooth operation of your Aikeedo installation.
### Troubleshooting Cron Issues
If you encounter problems with subscription renewals:
1. Verify the cron task configuration is correct
2. Check your server logs for error messages related to the cron job
3. Ensure the PHP path (`/usr/bin/php`) is correct for your server
4. Verify file permissions are correct for the web server user
Consult with your server administrator for server-specific issues. For additional help, see the [Troubleshooting & FAQ](/setup/troubleshooting) page.
## Configuring Your Site Domain
Properly setting your site domain is crucial for system functionality.
### Steps to Set Your Site Domain
1. **Access the Admin Panel:** Log in to your Aikeedo admin dashboard.
2. **Navigate to Settings:** Click on **Settings** in the sidebar, then select **General**.
3. **Locate the "Site Domain" Field:** You'll find this in the Website section.
4. **Enter Your Domain:** Input your domain name without the protocol (http\:// or https\://) and without any paths.
Include only the domain, subdomain (if any), and port number (if non-standard). Do not include protocols or paths. Omit the port number if it's 80 (HTTP) or 443 (HTTPS).
### SSL Configuration
Enabling SSL is highly recommended for security and is required for certain features.
1. Find the "Is SSL Enabled?" toggle in the General Settings.
2. If you have an SSL certificate installed on your server, switch this to "Yes."
Only enable SSL if you have properly installed an SSL certificate on your server. SSL is required for features like secure payment gateways.
If you haven't set up SSL yet, consider using a service like Let's Encrypt for a free SSL certificate. Many hosting providers also offer one-click SSL setup.
## Completing the "Getting Started" Section
After configuring your cron job and site domain, you'll want to complete the "Getting Started" section in your admin dashboard.
Completing the "Getting Started" section is crucial for ensuring all aspects of your Aikeedo installation are properly configured. Don't skip this step!
## Next Steps
After completing these initial setup steps, proceed to configure your application:
Complete guide to configuring all your Aikeedo settings
Or jump directly to specific configuration areas:
* [Email Settings](/email/mailer) - Configure email delivery
* [Billing Setup](/billing/overview) - Set up subscriptions and pricing
* [Integrations](/integrations/openai) - Connect AI providers and services
* [Branding](/website-basics/branding) - Customize your platform's appearance
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Installation Guide
Source: https://docs.aikeedo.com/setup/installation
Step-by-step guide to install Aikeedo on your server, configure the public directory, and complete the installation wizard.
This guide will walk you through the process of installing Aikeedo on your server.
**cPanel Users:** If you're using cPanel hosting, you MUST follow our dedicated [cPanel Installation Guide](/setup/installation/cpanel) instead of this guide. The steps are significantly different and this guide will not work for cPanel installations.
**Are you using cPanel?** Click here for the correct installation guide for cPanel hosting.
## Before You Start
1. Ensure your server meets our [requirements](/overview/server-requirements).
2. Prepare a database and note down its name, username, and password.
## Understanding the Public Directory
It's crucial to understand how Aikeedo's file structure works with your domain:
* Aikeedo has a `public` directory that contains web-accessible files.
* Your domain should point to this `public` directory, not the root of all Aikeedo files.
* The `public` directory cannot be moved; it must remain within the Aikeedo file structure.
Example:
If you extract Aikeedo files to `/var/www/yourdomain.com`, your domain must point to `/var/www/yourdomain.com/public`.
Consult your hosting provider's documentation on how to set the correct document root for your domain.
## Installation Steps
1. Locate the `aikeedo-server-files.zip` in your Aikeedo package.
2. Upload this zip file to your server, outside of any existing web-accessible directories.
If you can't find the zip file, refer to our [File Structure](/overview/file-structure) guide.
Extract the contents of `aikeedo-server-files.zip` so that the `public` folder ends up in your domain's root directory.
Example:
```
/var/www/yourdomain.com/
├── bin/
├── bootstrap/
├── ci/
├── config/
├── ...
├── public/
└── ... (other Aikeedo directories and files)
```
Set your domain's document root to point to the `public` directory within the extracted Aikeedo files.
Example:
If Aikeedo is extracted to `/var/www/yourdomain.com`, set the document root to `/var/www/yourdomain.com/public`.
This step often requires configuration in your web server (e.g., Apache, Nginx) or hosting control panel.
1. Open your web browser.
2. Navigate to `https://yourdomain.com`.
3. You should be automatically redirected to `https://yourdomain.com/install`.
4. If redirected successfully, you should see the Aikeedo installation wizard.
If you're not automatically redirected to the install page, it's likely that your domain is not correctly pointing to the `public` directory. In this case, you might see a 403 error. Double-check your domain's document root configuration to ensure it's set to the `public` folder within your Aikeedo installation.
The installation wizard will guide you through the setup process:
1. Provide Database Credentials:
* Enter the database name, username, and password.
2. Enter your Aikeedo Purchase Code:
* This is the unique code you received when you purchased Aikeedo.
3. Set up Admin Account Details:
* Create your administrator account for managing Aikeedo.
Carefully follow each step of the wizard, ensuring all information is entered accurately. This will ensure a smooth and successful installation of your Aikeedo instance.
Once you have provided all the necessary information, the installation wizard will finalize the setup:
1. The wizard will process your inputs and configure Aikeedo accordingly.
2. Wait patiently as the system completes the installation process.
3. Upon completion, you'll see a success message confirming that Aikeedo has been successfully installed.
4. Verify that you can access the Aikeedo dashboard or main page without any errors.
5. Confirm that the installation was successful by checking key features or settings.
Congratulations! You're now ready to explore Aikeedo and leverage its powerful capabilities for your projects.
## Troubleshooting
If you encounter any issues during the installation process:
1. Double-check that you've followed each step correctly
2. Ensure all file permissions are set correctly
3. Verify that your server meets all the [requirements](/overview/server-requirements)
4. Check the [Troubleshooting & FAQ](/setup/troubleshooting) page for solutions to common issues
## Next Steps
After successfully installing Aikeedo:
1. Follow our [Initial Setup Guide](/setup/initial-setup) to configure your new installation.
2. Explore [Website Basics](/website-basics/essentials) to learn about key Aikeedo features.
3. Consider setting up [email](/email/mailer) for your Aikeedo instance.
Congratulations on installing Aikeedo! We're excited for you to start leveraging its powerful features.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Installing Aikeedo in cPanel
Source: https://docs.aikeedo.com/setup/installation/cpanel
Learn how to install Aikeedo on your cPanel hosting, whether you're using a main domain, add-on domain, or subdomain.
Welcome to the Aikeedo installation guide for cPanel! This comprehensive tutorial will walk you through the process of setting up Aikeedo on your cPanel hosting environment. Whether you're using a main domain, an add-on domain, or a subdomain, we've got you covered.
## Before You Start
1. Ensure your server meets our [requirements](/overview/server-requirements).
2. Prepare a database and note down its name, username, and password.
3. Have your Aikeedo purchase code ready.
## Understanding the Public Directory
It's crucial to understand how Aikeedo's file structure works with your domain:
* Aikeedo has a `public` directory that contains web-accessible files.
* Your domain should point to this `public` directory, not the root of all Aikeedo files.
* The `public` directory cannot be moved; it must remain within the Aikeedo file structure.
In cPanel, this typically meanss:
* For main domains: copying the contents of the `public` folder to your `public_html` directory.
* For add-on domains or subdomains: setting the document root to the `public` folder within your Aikeedo installation.
## Installation Process
Follow these steps to install Aikeedo on your main domain in cPanel:
* Locate the `aikeedo-server-files.zip` archive in your Aikeedo package.
* Upload this archive to the directory one level above your `public_html` folder.
For example, if your `public_html` path is `/home/username/public_html`, upload to `/home/username/`.
If you can't find the zip file, refer to our [File Structure](/overview/file-structure) guide.
Once uploaded, extract the contents of `aikeedo-server-files.zip` in the same directory.
* Find the `public` folder within the extracted files (e.g., `/home/username/public`).
* **Copy** (do not move) all contents from this `public` folder to your `public_html` directory.
* Make sure to include hidden files and folders (such as .htaccess file or .vite directory).
* Locate the `.env.example` file in your Aikeedo root directory (e.g., `/home/username/`).
* Find the line that contains `# PUBLIC_DIR=public_html`.
* Uncomment this line by removing the `#` symbol and the space after it.
* The final version of this line should look like:
```env .env.example theme={null}
PUBLIC_DIR=public_html
```
* Save the changes to the file.
* Open your web browser and navigate to your domain.
* You'll be automatically redirected to the installation page at `/install`.
* Follow the on-screen instructions in the installation wizard.
* You'll need to provide:
* Database credentials
* Your Aikeedo purchase code
* Admin account details
* Complete all steps as guided by the wizard.
Follow these steps to install Aikeedo on an add-on domain or subdomain in cPanel:
* Locate the `aikeedo-server-files.zip` archive in your Aikeedo package.
* Upload this archive to your add-on domain or subdomain root directory.
For example: `/home/username/yourdomain.com/`
If you can't find the zip file, refer to our [File Structure](/overview/file-structure) guide.
Once uploaded, extract the contents of `aikeedo-server-files.zip` in the same directory.
* In cPanel, navigate to **Domains** > **Addon Domains** (or **Subdomains**).
* Click **Manage** next to your domain.
* Update the **Document Root** to point to the `public` folder within your extracted files.
For example: `/home/username/yourdomain.com/public`
* Save your changes.
* Open your web browser and navigate to your add-on domain or subdomain URL.
* You'll be automatically redirected to the installation page at `/install`.
* Follow the on-screen instructions in the installation wizard.
* You'll need to provide:
* Database credentials
* Your Aikeedo purchase code
* Admin account details
* Complete all steps as guided by the wizard.
## Important Notes
* Throughout this guide, we use absolute paths (e.g., `/home/username/`). In cPanel's file manager, you might not see the `/home/username/` part. For example, `/home/username/public_html` might appear as just `/public_html`.
* Always ensure you're working in the correct directory for your specific domain setup.
* The `public` directory contains all files that should be publicly accessible. Don't confuse this with cPanel's default `public_html` directory.
## Troubleshooting
If you encounter any issues during the installation process:
1. Double-check that you've followed each step correctly.
2. Ensure all file permissions are set correctly.
3. Verify that your server meets all the [requirements](/overview/server-requirements).
4. For more detailed troubleshooting steps, please refer to our dedicated [Troubleshooting Guide](/setup/troubleshooting).
## Next Steps
After successfully installing Aikeedo:
1. Follow our [Initial Setup Guide](/setup/initial-setup) to configure your new installation.
2. Explore [Website Basics](/website-basics/essentials) to learn about key Aikeedo features.
3. Consider setting up [email](/email/mailer) for your Aikeedo instance.
Congratulations! You've successfully installed Aikeedo on your cPanel hosting. You're now ready to harness the power of AI for your content creation needs.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Local Installation Guide
Source: https://docs.aikeedo.com/setup/installation/local
Learn how to install Aikeedo on your local machine for development, testing, or personal use. This guide covers various setup options, including using PHP's built-in server and popular local development environments.
This guide will walk you through the process of installing Aikeedo on your local machine. This is ideal for development, testing, or personal use.
## Prerequisites
Before you begin, ensure you have:
1. Downloaded the Aikeedo package
2. Familiarity with command-line operations
3. A local development environment (we'll cover different options)
## Option 1: Using PHP's Built-in Server
This option is suitable if you already have PHP and MySQL installed and accessible via the command line.
Ensure you have:
* PHP 8.2 or higher installed and accessible via command line
* MySQL 8.0 or higher installed and running
1. Locate the `aikeedo-server-files.zip` in your Aikeedo package.
2. Extract the contents to a directory of your choice, e.g., `C:\aikeedo` or `/home/user/aikeedo`.
1. Open your MySQL client or command-line tool:
* For command-line users, open your terminal and type `mysql -u root -p` to access MySQL.
* For GUI users, open tools like phpMyAdmin, MySQL Workbench, or HeidiSQL.
2. Create a new database for Aikeedo:
* In the MySQL command line or GUI query window, execute the following SQL command:
```sql theme={null}
CREATE DATABASE aikeedo;
```
* This creates a new, empty database named 'aikeedo' that will store all your Aikeedo data.
3. Create a new user (recommended for security):
```sql theme={null}
CREATE USER 'aikeedo_user'@'localhost' IDENTIFIED BY 'your_secure_password';
GRANT ALL PRIVILEGES ON aikeedo.* TO 'aikeedo_user'@'localhost';
FLUSH PRIVILEGES;
```
Replace 'your\_secure\_password' with a strong, unique password.
4. Note down the following information for the installation wizard:
* Database name: aikeedo
* Username: aikeedo\_user (or 'root' if you skipped step 3)
* Password: The password you set (or your root password if using root)
* Host: localhost (in most cases)
If you're not comfortable with database management, consider using a database administration tool like phpMyAdmin, which often comes pre-installed with local development environments like XAMPP or MAMP.
To start the PHP development server:
1. Open your terminal or command prompt.
2. Navigate to the root directory of your Aikeedo installation. This is the directory where you extracted the Aikeedo files, e.g., `C:\aikeedo` or `/home/user/aikeedo`.
3. Once in the root directory, run the following command:
```bash theme={null}
php -S localhost:8000 -t public
```
This command does the following:
* `php -S` starts PHP's built-in development server
* `localhost:8000` sets the server to listen on localhost at port 8000
* `-t public` sets the document root to the `public` directory within your Aikeedo installation
After running this command, you should see output indicating that the server is running. Keep this terminal window open to keep the server active.
Open your web browser and navigate to `http://localhost:8000`. You should see the Aikeedo installation wizard.
1. Provide the database credentials you noted earlier.
2. Enter your Aikeedo Purchase Code.
3. Set up your admin account details.
4. Follow any additional steps as prompted by the wizard.
## Option 2: Using Local Development Environments
This option is suitable if you prefer using pre-configured local development environments like XAMPP, MAMP, or Laragon.
Download and install XAMPP from the [official website](https://www.apachefriends.org/).
1. Open `C:\xampp\apache\conf\extra\httpd-vhosts.conf` (Windows) or `/Applications/XAMPP/etc/extra/httpd-vhosts.conf` (macOS).
2. Add a new virtual host:
```apache theme={null}
DocumentRoot "C:/xampp/htdocs/aikeedo/public"
ServerName aikeedo.test
AllowOverride All
Require all granted
```
1. Open your hosts file (`C:\Windows\System32\drivers\etc\hosts` on Windows or `/etc/hosts` on macOS).
2. Add the following line:
```
127.0.0.1 aikeedo.test
```
Extract the contents of `aikeedo-server-files.zip` to `C:\xampp\htdocs\aikeedo` (Windows) or `/Applications/XAMPP/htdocs/aikeedo` (macOS).
1. Open phpMyAdmin (usually at `http://localhost/phpmyadmin`).
2. Create a new database named `aikeedo`.
Restart Apache in XAMPP, then navigate to `http://aikeedo.test` in your browser. You should see the installation wizard.
Follow the on-screen installation wizard to complete the setup.
Download and install MAMP from the [official website](https://www.mamp.info/).
1. Open MAMP's Apache configuration file.
2. Add a new virtual host:
```apache theme={null}
DocumentRoot "/Applications/MAMP/htdocs/aikeedo/public"
ServerName aikeedo.test
AllowOverride All
Require all granted
```
1. Open `/etc/hosts`.
2. Add the following line:
```
127.0.0.1 aikeedo.test
```
Extract the contents of `aikeedo-server-files.zip` to `/Applications/MAMP/htdocs/aikeedo`.
1. Open phpMyAdmin (usually at `http://localhost:8888/phpMyAdmin`).
2. Create a new database named `aikeedo`.
Restart Apache in MAMP, then navigate to `http://aikeedo.test` in your browser. You should see the installation wizard.
Follow the on-screen installation wizard to complete the setup.
Download and install Laragon from the [official website](https://laragon.org/).
Extract the contents of `aikeedo-server-files.zip` to `C:\laragon\www\aikeedo`.
1. Right-click the Laragon tray icon and select "Apache > sites-enabled > Add".
2. Enter the following configuration:
```apache theme={null}
DocumentRoot "C:/laragon/www/aikeedo/public"
ServerName aikeedo.test
AllowOverride All
Require all granted
```
3. Save the file and restart Apache in Laragon.
1. Open HeidiSQL (included with Laragon).
2. Create a new database named `aikeedo`.
Navigate to `http://aikeedo.test` in your browser. You should see the installation wizard.
Follow the on-screen installation wizard to complete the setup.
Remember, Aikeedo must be installed in the domain root. Subpaths like `http://localhost/subdirectory` are not supported.
## Troubleshooting
If you encounter any issues during local installation:
1. Ensure your local environment meets all [server requirements](/overview/server-requirements).
2. Check that all file permissions are set correctly.
3. Verify that your virtual host and hosts file configurations are correct.
4. For more detailed troubleshooting steps, refer to our [Troubleshooting Guide](/setup/troubleshooting).
## Next Steps
After successfully installing Aikeedo locally:
1. Explore the [Initial Setup Guide](/setup/initial-setup) to configure your local instance.
2. Check out our [Local Development Guide](/development/local-development) for tips on developing with Aikeedo.
Congratulations on setting up Aikeedo locally! You're now ready to start developing or testing with your local instance.
## Need Help?
If you need assistance with Aikeedo:
Get expert help from our team with a paid support subscription
Check common issues and solutions
# Troubleshooting
Source: https://docs.aikeedo.com/setup/troubleshooting
Solutions to common Aikeedo installation, configuration, and runtime issues including database errors, asset loading problems, and model configuration.
## Overview
This guide provides solutions to common issues users encounter when installing, configuring, and running Aikeedo. Issues are organized by category for easy navigation.
## Before You Begin
Before troubleshooting, verify that:
1. Your server meets all [server requirements](/overview/server-requirements)
2. You have the latest version of Aikeedo files
3. You've followed the [installation guide](/setup/installation) carefully
4. You've completed [initial setup](/setup/initial-setup) steps
## Quick Navigation
Find your issue and click to jump to the solution:
### Installation Issues
* [Assets Not Loading (CSS, JS, Images)](#assets-not-loading)
* [404 or 403 Errors During Installation](#404-or-403-errors-during-installation)
* [Database Connection Issues](#database-connection-issues)
* [500 Internal Server Error During Installation](#500-internal-server-error-during-installation)
* [Installation Wizard Not Loading](#installation-wizard-not-loading)
### Configuration Issues
* ["Does Not Allow the Value" Error](#"does-not-allow-the-value"-error)
* [Cannot Select Models or Send Prompts](#cannot-select-models-or-send-prompts)
* [500 Error on Image Generator or Video Generator](#500-error-on-image-generator-or-video-generator)
* [Save Button Does Nothing](#save-button-does-nothing)
### Database Issues
* [MySQL Server Has Gone Away](#mysql-server-has-gone-away)
### Feature-Specific Issues
* [Auto-Generated Titles Don't Match Content Language](#auto-generated-titles-don't-match-content-language)
* [Uploaded Files Return 404 Not Found Errors](#uploaded-files-return-404-not-found-errors)
* [Generated Images/Videos Not Loading](#generated-images%2Fvideos-not-loading)
***
## Installation Issues
### Assets Not Loading
**Symptoms:**
* Installation page appears without styling
* Images, CSS, and JavaScript files fail to load
* Browser shows 404 errors for asset files
**Cause:**
The domain is not correctly pointing to the `public` directory.
**Solution:**
Ensure your domain points to the `public` directory within your Aikeedo installation:
```
/path/to/aikeedo/public
```
Not to the root Aikeedo directory.
Visit your domain (e.g., `https://yourdomain.com`). You should be automatically redirected to `https://yourdomain.com/install`.
If the redirect works, your domain is configured correctly.
If you're using cPanel and cannot change the document root for your main domain, follow the dedicated [cPanel Installation Guide](/setup/installation/cpanel).
The cPanel installation process differs from the standard installation due to control panel limitations.
**Related Documentation:**
* [Installation Guide](/setup/installation)
* [cPanel Installation](/setup/installation/cpanel)
***
### 404 or 403 Errors During Installation
**Symptoms:**
* 404 error when accessing `/install`
* 403 Forbidden error on home page
* Installation wizard not accessible
**Cause:**
Domain document root not pointing to the `public` directory, missing files, incorrect permissions, or web server configuration issues preventing proper request routing.
**Solution:**
Ensure your domain's document root points to the `public` folder:
**For cPanel:**
* Main domain: Points to `public_html` by default (this is correct)
* Ensure Aikeedo's `public` folder contents are copied to `public_html`
* Add-on domains/subdomains: Update document root in cPanel to point to the `public` folder
**For VPS/Dedicated:**
* Set document root to `/path/to/aikeedo/public`
Accessing your domain should automatically redirect you to `/install` if the document root is correctly configured.
Configure your web server to serve PHP applications and rewrite all requests to `/public/index.php`:
Ensure your server block is configured correctly:
```nginx /etc/nginx/sites-available/aikeedo theme={null}
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
root /path/to/aikeedo/public;
# Add index.php to the list
index index.php index.html index.htm index.nginx-debian.html;
# Increase the maximum upload size
client_max_body_size 256M;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to index.php
try_files $uri $uri/ /index.php$is_args$query_string;
}
# Pass PHP scripts to FastCGI server
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Fix for PHP files downloading instead of execute
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
# With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php-fpm.sock;
# With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
fastcgi_connect_timeout 300s;
fastcgi_send_timeout 300s;
fastcgi_read_timeout 300s;
}
# Deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
location ~ /\.ht {
deny all;
}
}
```
After configuring, test your Nginx configuration: `sudo nginx -t` and reload: `sudo systemctl reload nginx`
Adjust the `fastcgi_pass` socket path to match your PHP-FPM configuration. Common locations:
* `/run/php/php-fpm.sock` (generic)
* `/run/php/php8.2-fpm.sock` (version-specific)
* `/var/run/php/php8.2-fpm.sock` (alternative path)
Ensure your virtual host is configured correctly with proper rewrite rules:
```apache /etc/apache2/sites-available/aikeedo.conf theme={null}
ServerName yourdomain.com
ServerAlias www.yourdomain.com
DocumentRoot /path/to/aikeedo/public
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
```
Ensure `mod_rewrite` is enabled: `sudo a2enmod rewrite` and reload Apache: `sudo systemctl reload apache2`
The server configurations provided above are minimal samples. Consult with your server administrator for the correct configuration for your specific server environment, especially if you're using load balancers, reverse proxies, or have specific security requirements.
If you're using Apache, ensure the `.htaccess` file is present in the `public` directory. Aikeedo includes this file by default with the correct rewrite rules.
Hidden files may not be visible by default. Enable "Show hidden files" in your FTP client or file manager if you need to verify the file exists.
Verify all Aikeedo files are extracted correctly and the `public` directory exists with all necessary files.
Ensure the web server has appropriate permissions:
```bash theme={null}
# Set directory permissions
find /path/to/aikeedo -type d -exec chmod 755 {} \;
# Set file permissions
find /path/to/aikeedo -type f -exec chmod 644 {} \;
```
Double-check that all virtual host settings are correctly configured:
* **Document Root**: Must point to `/path/to/aikeedo/public` (not the root directory)
* **Server Name/Alias**: Must match your domain exactly
* **PHP Handler**: Configured correctly (PHP-FPM for Nginx, mod\_php or PHP-FPM for Apache)
* **Rewrite Rules**: Enabled and properly configured to route all requests to `index.php`
After making changes:
1. Test web server configuration:
* Nginx: `sudo nginx -t`
* Apache: `sudo apache2ctl configtest`
2. Reload your web server:
```bash theme={null}
# Nginx
sudo systemctl reload nginx
# Apache
sudo systemctl reload apache2
```
3. Visit your domain - you should be automatically redirected to `/install`
If you're redirected to `/install`, your server configuration is correct.
All requests must be rewritten to `/public/index.php` for the application to function correctly. Without proper rewrite rules, you'll encounter 404 errors on all routes except the home page.
***
### Database Connection Issues
**Symptoms:**
* Cannot connect to database during installation
* Database error messages in installation wizard
* Installation fails at database configuration step
**Cause:**
Incorrect database credentials, missing database, or insufficient permissions.
**Solution:**
1. **Verify Database Credentials**
* **Database name**: Must be exact (case-sensitive)
* **Username**: Database user with proper permissions
* **Password**: Correct password for the database user
* **Host**: Usually `localhost`, but may be different on some hosts
2. **Check Database Server**
* Ensure the database server (MySQL/MariaDB) is running
* Verify the database is accessible from your web server
* Confirm the database exists and is empty
3. **Verify User Permissions**
The database user must have these permissions:
* SELECT, INSERT, UPDATE, DELETE
* CREATE, DROP, INDEX, ALTER
If unsure, grant ALL PRIVILEGES to the database user for the specific database.
4. **Check PHP Extensions**
Verify that required PHP database extensions are installed:
* `mysqli` for MySQL
* `pdo_mysql` for PDO support
Check with:
```bash theme={null}
php -m | grep -i mysql
```
If problems persist, try creating a new database and user with full permissions.
***
### 500 Internal Server Error During Installation
**Symptoms:**
* HTTP 500 error before or during installation
* Blank page with 500 status code
* Generic server error message
**Cause:**
PHP configuration issues, incorrect file permissions, or insufficient server resources.
**Solution:**
Review server error logs for specific error messages:
**Apache:**
```bash theme={null}
tail -f /var/log/apache2/error.log
# or
tail -f /var/log/httpd/error_log
```
**Nginx:**
```bash theme={null}
tail -f /var/log/nginx/error.log
```
Error logs will reveal the exact issue.
Ensure you're running PHP 8.2 or later:
```bash theme={null}
php -v
```
Verify all required extensions are installed:
```bash theme={null}
php -m
```
```bash theme={null}
# Set ownership (replace www-data with your web server user)
chown -R www-data:www-data /path/to/aikeedo
# Set directory permissions
find /path/to/aikeedo -type d -exec chmod 755 {} \;
# Set file permissions
find /path/to/aikeedo -type f -exec chmod 644 {} \;
```
Common web server users: `www-data` (Ubuntu/Debian), `apache` (CentOS/RHEL), `nginx` (Nginx)
If using PHP-FPM:
```bash theme={null}
sudo systemctl restart php8.2-fpm
```
Replace `8.2` with your PHP version.
Edit your `php.ini` file:
```ini theme={null}
memory_limit = 256M
```
Restart your web server after making changes.
***
### Installation Wizard Not Loading
**Symptoms:**
* Blank white page when accessing installation
* Installation wizard doesn't appear
* JavaScript errors in browser console
**Cause:**
PHP errors, insufficient server resources, or missing files.
**Solution:**
1. **Check PHP Error Logs**: Review PHP error logs for fatal errors or warnings
2. **Clear Browser Cache**: Hard refresh (Ctrl+Shift+R or Cmd+Shift+R) or try a different browser
3. **Verify SSL Certificate**: If using HTTPS, ensure your SSL certificate is valid
4. **Check File Permissions**: Verify web server can read all files in the `public` directory
5. **Confirm PHP Extensions**: Ensure all required extensions are installed (see [Server Requirements](/overview/server-requirements))
6. **Check Server Resources**: Verify sufficient CPU and RAM are available
Try accessing the site from an incognito/private browsing window to rule out browser cache issues.
***
## Configuration Issues
### "Does Not Allow the Value" Error
**Symptoms:**
* Error message: "Does not allow the value"
* Occurs after installation when trying to use features
**Cause:**
The Site Domain value is missing in General Settings.
**Solution:**
1. Log in to the admin panel
2. Navigate to **Settings > General**
3. Locate the "Site Domain" field
4. Enter your domain without protocol or paths (e.g., `yourdomain.com`)
5. Enable SSL if you have an SSL certificate
6. Click "Save changes"
**Related Documentation:**
* [Initial Setup - Configuring Site Domain](/setup/initial-setup#configuring-your-site-domain)
***
### Cannot Select Models or Send Prompts
**Symptoms:**
* Browser console shows JavaScript errors
* Unable to select AI models in dropdown
* Cannot send prompts or generate content
* Tools missing from sidebar
**Cause:**
Features and models are not enabled globally, or plan configuration is incomplete.
**Solution:**
1. Navigate to **Settings > Features** in the admin panel
2. Enable the features you want to offer (Chat, Writer, Imagine, etc.)
3. Click "Save changes"
Enabling features globally makes them available but they still need to be configured in plans.
1. Go to **Settings > Models**
2. Enable the AI models you want to make available
3. Click "Save changes"
1. Navigate to the **Plans** page in the admin panel
2. Find the plan your users are subscribed to
3. Edit the plan configuration:
* Enable desired features
* Enable desired models
* Set credit allocations
4. **Important:** Check the "Update snapshots" box before saving
5. Click "Save changes"
If you don't check "Update snapshots," changes will only apply to new subscriptions, not existing ones.
1. Log in as a test user
2. Verify tools appear in the sidebar
3. Test selecting a model and generating content
Users should now be able to access features and generate content.
**Related Documentation:**
* [Plans, Snapshots & Subscriptions](/billing/plans-snapshots-subscriptions)
* [Application Configuration](/setup/configuration)
***
### 500 Error on Image Generator or Video Generator
**Symptoms:**
* Imagine (image generator) page returns HTTP 500 error
* Video generator page returns HTTP 500 error
* Server error logs show credit ratio issues
**Cause:**
Credit ratios are not configured for all enabled models.
**Solution:**
1. Navigate to **Settings > Credit Ratios** in the admin panel
2. Review all enabled AI models
3. Ensure **every enabled model** has a credit ratio configured
4. Set appropriate credit values for each model
5. Click "Save changes"
If you're unsure about credit ratios, start with the default suggestions and adjust based on your pricing strategy.
**Related Documentation:**
* [Unified Credit System](/billing/unified-credit-system)
* [Application Configuration](/setup/configuration)
***
### Save Button Does Nothing
**Symptoms:**
* Clicking "Save changes" or other save buttons in the admin panel has no effect
* No error message appears
* Page appears to be unresponsive after clicking save
* Changes are not saved
**Cause:**
Most commonly a backend PHP error that's preventing the request from completing. Can also be a frontend JavaScript issue.
**Solution:**
Before clicking the save button, check for frontend errors:
1. Open your browser's developer console:
* **Chrome/Edge**: Press `F12` or `Ctrl+Shift+I` (Windows/Linux) / `Cmd+Option+I` (Mac)
* **Firefox**: Press `F12` or `Ctrl+Shift+K` (Windows/Linux) / `Cmd+Option+K` (Mac)
* **Safari**: Enable Developer menu first, then press `Cmd+Option+I`
2. Look for any red error messages in the console
3. Review error details - they often indicate the specific issue
If you see errors in the console before clicking save, the issue is likely frontend-related. Note the error messages for troubleshooting.
If no frontend errors are visible, first check server-level error logs, as the issue might occur before the request reaches PHP:
1. **Locate your web server error log**:
```bash theme={null}
# Apache
/var/log/apache2/error.log
# or
/var/log/httpd/error_log
# Nginx
/var/log/nginx/error.log
```
2. **View recent errors**:
```bash theme={null}
tail -f /path/to/error.log
```
3. **Click the save button** while monitoring the log
4. **Review any error messages** that appear
Server-level errors can include issues like request timeouts, connection problems, or web server configuration errors that prevent requests from reaching PHP.
If no server-level errors are found, check PHP error logs:
1. **Locate your PHP error log** (common locations):
```bash theme={null}
# PHP-FPM
/var/log/php8.2-fpm.log
# or check php.ini for error_log location
# You can find it with: php -i | grep error_log
```
2. **View recent errors**:
```bash theme={null}
tail -f /path/to/php-error.log
```
3. **Click the save button** while monitoring the log
4. **Review the error message** that appears in the log
The error log will show the exact PHP error preventing the save operation from completing. Common issues include database errors, permission problems, or missing dependencies.
Alternatively, enable DEBUG mode to get more detailed error information:
1. **Enable DEBUG mode** from the admin panel:
* Navigate to **Status** page in the admin panel
* Enable DEBUG mode using the toggle or button provided
2. **Open your browser's developer console** (see step 1 above)
3. **Navigate to the Network tab** in the developer console
4. **Click the save button** again
5. **Look for the API request** that was made when you clicked save (usually shows as a POST request)
6. **Click on the request** to view details:
* Check the **Status Code** (should be between 200 and 299 for success, 4xx/5xx for errors)
* Check the **Response** tab to see the response body
* Review any error messages in the response
Remember to disable DEBUG mode from the Status page after troubleshooting, as it can expose sensitive information in production environments.
Based on the error information you've gathered:
* **Database errors**: Check database connection and permissions
* **Permission errors**: Verify file/directory permissions (see [500 Internal Server Error](#500-internal-server-error-during-installation))
* **Missing dependencies**: Ensure all required PHP extensions are installed
* **Frontend JavaScript errors**: Check for conflicting scripts or browser compatibility issues
The specific error message will guide you to the exact solution. Common fixes include updating file permissions, fixing database credentials, or installing missing PHP extensions.
**Related Documentation:**
* [500 Internal Server Error During Installation](#500-internal-server-error-during-installation) - Permission and PHP configuration issues
* [Database Connection Issues](#database-connection-issues) - Database-related errors
***
## Database Issues
### MySQL Server Has Gone Away
**Symptoms:**
* Error message: "MySQL server has gone away"
* Database connection drops during requests
* Long-running operations fail
**Cause:**
MySQL server timeout or packet size limitations.
**Solution:**
**Problem:** MySQL server times out and closes the connection.
**Solution:** Update your `my.cnf` configuration file:
```ini theme={null}
[mysqld]
wait_timeout = 28800
```
This sets the timeout to 8 hours. Adjust as needed for your use case.
**Problem:** MySQL receives packets that are too large or incorrect.
**Solution:** Increase the max packet size in `my.cnf`:
```ini theme={null}
[mysqld]
max_allowed_packet = 128M
```
This allows larger data packets to be processed.
**Problem:** InnoDB log file size is insufficient.
**Solution:** Increase the log file size in `my.cnf`:
```ini theme={null}
[mysqld]
innodb_log_file_size = 128M
```
Or larger, depending on your data volume.
After modifying `my.cnf`, you must restart your MySQL server for changes to take effect:
```bash theme={null}
sudo systemctl restart mysql
```
or
```bash theme={null}
sudo service mysql restart
```
**Finding my.cnf:**
Common locations for the MySQL configuration file:
* Linux: `/etc/my.cnf` or `/etc/mysql/my.cnf`
* macOS: `/etc/my.cnf`
* Windows: `C:\ProgramData\MySQL\MySQL Server X.X\my.ini`
If you're on shared hosting and don't have access to `my.cnf`, contact your hosting provider to request these changes.
***
## Feature-Specific Issues
### Auto-Generated Titles Don't Match Content Language
**Symptoms:**
* AI generates titles in Spanish when content is in another language
* Title language doesn't match the generated content language
**Cause:**
Before v5.0, the title generator trimmed the content it read too aggressively and nudged some models toward English, so short or non-English content often got a title in the wrong language. v5.0 fixes both: titles are written in the language of the content, and fall back to the user's language only when the content is too short to tell.
**Solution:**
See [How to Update](/versioning/how-to-update).
Some models follow language instructions better than others, which matters most for non-English content.
1. Navigate to **Settings > Features > Utilities** in the admin panel
2. Under **Title generator**, choose a different **Model**
3. Click "Save changes" and test title generation again
Since v5.0 the title generator model is one setting for the whole installation. Plans no longer override it.
***
### Uploaded Files Return 404 Not Found Errors
**Symptoms:**
* Uploaded files show "not found" or 404 errors
* Generated files (images, videos, audio, documents etc.) are inaccessible
* Files exist in storage but cannot be accessed via URL
* Browser shows 404 when clicking file links
**Cause:**
File grouping enabled on local storage, secure URLs enabled, incorrect document root configuration, or file permission issues.
**Solution:**
If you're experiencing file access issues with local storage, try disabling file grouping:
1. Log in to the admin panel
2. Navigate to **Settings > Storage**
3. Locate the "File Grouping" option
4. Change the value to **"None"**
5. Click "Save changes"
File grouping and secure URLs are optimized for cloud storage (S3, DigitalOcean Spaces, etc.) and may cause compatibility issues on some local storage configurations, particularly on cPanel-based hosting. If files are inaccessible, disabling these options often resolves the issue.
If you're using local storage without special security requirements:
1. In **Settings > Storage**
2. Locate the "Secure URLs" option
3. **Turn off** the "Secure URLs" option
4. Click "Save changes"
If files were uploaded with Secure URLs enabled, you may need to re-upload them after disabling this option.
If your domain's document root is set to `public_html` instead of `public`:
1. Open your `.env` file in the root directory
2. Find the line with `PUBLIC_DIR` (it may be commented out with `#`)
3. Ensure it's set correctly:
```bash theme={null}
PUBLIC_DIR=public_html
```
4. Remove the `#` symbol if present at the beginning
5. Save the file
This is particularly important for cPanel users where the default document root is `public_html`.
Ensure the web server has read access to uploaded files:
```bash theme={null}
# Navigate to your Aikeedo directory
cd /path/to/aikeedo
# Set correct permissions for storage directory
chmod -R 755 storage/
# Set correct ownership (replace www-data with your web server user)
chown -R www-data:www-data storage/
```
Common web server users:
* Ubuntu/Debian: `www-data`
* CentOS/RHEL: `apache`
* Nginx: `nginx`
If you're using cloud storage (AWS S3, DigitalOcean Spaces, etc.), CORS issues may prevent file access:
1. **Check your cloud storage CORS configuration:**
* Ensure your bucket allows requests from your domain
* Verify that the GET method is allowed (generally sufficient for file access)
* Check that the correct headers are permitted
2. **Example CORS configuration for AWS S3:**
```json theme={null}
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET"],
"AllowedOrigins": ["https://yourdomain.com", "https://www.yourdomain.com"]
}
]
```
3. **Verify your storage configuration:**
* Check **Settings > Storage** in your admin panel
* Ensure cloud storage credentials are correct
* Verify the bucket/container name and region are accurate
CORS misconfiguration can cause files to appear uploaded but return 404 errors when accessed. Always test file access after making CORS changes.
Check that your storage path is correctly configured:
1. Go to **Settings > Storage** in the admin panel
2. Verify the storage driver is set correctly (Local, S3, etc.)
3. If using local storage, ensure the path is correct
4. Test by uploading a new file
If the new file is accessible, the configuration is correct. You may need to re-upload older files.
After making configuration changes:
1. Navigate to **Status** page in the admin panel
2. Click "Clear cache" to clear the application cache
3. Test file access again
For production environments requiring secure file access or workspace-based file organization, cloud storage (S3, DigitalOcean Spaces) is recommended. File grouping and secure URL features are optimized for cloud storage and work most reliably in that environment, though they may also work on some local storage configurations.
**Related Documentation:**
* [Secure URLs](/advanced/secure-urls) - Configure secure file access
* [cPanel Installation](/setup/installation/cpanel) - Document root configuration for cPanel
***
### Generated Images/Videos Not Loading
**Symptoms:**
* Generated images or videos fail to display after creation
* Broken image/video placeholders appear instead of content
* 404 or 403 errors when accessing generated media files
* Files appear to generate successfully but cannot be viewed
**Cause:**
Incomplete initial setup, cloud storage CORS misconfiguration, incorrect `.env` file configuration (especially for cPanel installations), or Secure URLs/Group files settings enabled when using local storage.
**Solution:**
Ensure you've completed all steps in the [Initial Setup Guide](/setup/initial-setup):
1. Verify your site domain is configured correctly in **Settings > General**
2. Check that storage settings are properly configured in **Settings > File storage**
3. Ensure all required integrations are set up if using cloud storage
Skipping or incorrectly completing initial setup steps can prevent generated files from being accessible.
If you're using cloud storage (AWS S3, DigitalOcean Spaces, Cloudflare R2, etc.), CORS misconfiguration is a common cause:
1. **Access your cloud storage provider's console** (AWS S3, DigitalOcean, etc.)
2. **Navigate to your bucket/container settings**
3. **Review CORS configuration** - it must allow requests from your domain
4. **Update CORS rules** if needed:
5. **Save the CORS configuration**
6. **Test by generating a new image or video**
CORS rules must include your exact domain (with protocol: `https://` or `http://`). Wildcards may not work for all storage providers.
See the [Cloud Storage Overview](/integrations/cloud-storage/overview) and provider-specific guides for detailed CORS configuration instructions.
The **Secure URLs** and **Group files** options should only be enabled when using cloud storage (AWS S3, DigitalOcean Spaces, etc.):
1. Navigate to **Settings > File storage** in the admin panel
2. Review the **Secure URLs** and **Group files** settings
3. **If using local storage**, ensure both options are disabled:
* **Secure URLs**: Should be turned **Off**
* **Group files**: Should be set to **None**
4. **If using cloud storage**, these options can be enabled as needed
5. Click "Save changes"
**Secure URLs** and **Group files** are designed for cloud storage only. Enabling these options with local storage can cause generated files to be inaccessible.
Changing these settings only affects newly generated files. Files that were already generated with the previous settings will remain unchanged. If the issue stems from Secure URLs, previously generated files can be made accessible by fixing file permissions (see the "Verify Storage Path and Permissions" step below). Otherwise, you may need to regenerate images or videos after adjusting these settings.
If you're using cPanel, the `.env` file configuration is critical:
1. **Locate your `.env` file** in the Aikeedo root directory (not in `public_html`)
2. **Find the `PUBLIC_DIR` setting** - it may be commented out with `#`
3. **For main domain installations**, ensure it's set correctly:
```env theme={null}
PUBLIC_DIR=public_html
```
4. **Remove the `#` symbol** if the line is commented out
5. **Save the file**
If you're using cPanel and the `PUBLIC_DIR` setting is missing or incorrect, generated files will not be accessible. This is a critical step that must be completed during installation.
For add-on domains or subdomains in cPanel where the document root points directly to the `public` folder, you may not need to set `PUBLIC_DIR` or it should be set to `public`. Refer to the [cPanel Installation Guide](/setup/installation/cpanel) for your specific setup.
Check that your storage configuration is correct:
1. Navigate to **Settings > File storage** in the admin panel
2. Verify the storage driver is set correctly (Local, S3, etc.)
3. If using local storage, ensure the storage path is writable:
```bash theme={null}
# Check storage directory permissions
ls -la /path/to/aikeedo/public/uploads/
# Set correct permissions if needed
chmod -R 755 /path/to/aikeedo/public/uploads/
chown -R www-data:www-data /path/to/aikeedo/public/uploads/
```
4. If using cloud storage, verify:
* Bucket/container name is correct
* Region is correctly configured
* Access keys are valid and have proper permissions
* Bucket/container exists and is accessible
Test by generating a new image or video. If it loads correctly, the configuration is working.
**For cPanel users:** If you skipped or incorrectly completed the `.env` file configuration step during installation (specifically the `PUBLIC_DIR=public_html` setting), generated files will not load correctly. This is one of the most common causes of this issue on cPanel hosting. Refer to the [cPanel Installation Guide](/setup/installation/cpanel) and ensure you've completed the "Update the Environment File" step correctly.
**Related Documentation:**
* [Initial Setup Guide](/setup/initial-setup) - Complete setup configuration
* [cPanel Installation](/setup/installation/cpanel) - Critical `.env` file configuration
* [Cloud Storage Overview](/integrations/cloud-storage/overview) - Cloud storage setup and CORS configuration
* [Secure URLs](/advanced/secure-urls) - Secure file access configuration
***
## Additional Troubleshooting Steps
If you're still experiencing issues after trying the solutions above:
1. **Fresh Installation**: Consider a clean reinstall with fresh Aikeedo files
2. **Server Configuration**: Review web server configuration (Apache/Nginx) for PHP applications
3. **PHP Configuration**: Check `php.ini` for settings like:
* `max_execution_time`
* `upload_max_filesize`
* `post_max_size`
* `memory_limit`
4. **Hosting Environment**: Contact your hosting provider to ensure no server-side restrictions
## Getting Additional Help
If you've tried the solutions above and still need assistance:
### Before Contacting Support
1. **Check Error Logs:**
* Browser console (F12 in most browsers)
* Server error logs (`/var/log/apache2/error.log` or `/var/log/nginx/error.log`)
* Application logs
* PHP error logs
2. **Verify Configuration:**
* Review [Initial Setup](/setup/initial-setup)
* Check [Application Configuration](/setup/configuration)
* Verify [Server Requirements](/overview/server-requirements)
3. **Clear Caches:**
* Clear browser cache (Ctrl+Shift+R or Cmd+Shift+R)
* Clear application cache from admin panel Status page
### Need Additional Help?
Get expert help from our team with a paid support subscription
For support plan options and pricing, visit [https://aikeedo.com/support/](https://aikeedo.com/support/?utm_source=docs\&utm_medium=content).
**Before contacting support:**
* Ensure you have an active [support plan subscription](https://aikeedo.com/support/?utm_source=docs\&utm_medium=content)
* Have your Aikeedo license key ready for validation
**When contacting support, include:**
* Your Aikeedo license key (for purchase validation)
* Detailed description of the issue
* Complete error messages (screenshots if possible)
* Steps to reproduce the problem
* PHP version and server environment details
* Aikeedo version number
* Relevant error log entries
* What troubleshooting steps you've already tried
Support is only available to users with active support plan subscriptions. Requests without valid license keys or active support plans will not be processed.
The more detailed information you provide, the faster our support team can help resolve your issue.
***
## Related Guides
* [Installation Guide](/setup/installation)
* [Initial Setup](/setup/initial-setup)
* [Application Configuration](/setup/configuration)
* [Server Requirements](/overview/server-requirements)
* [Plans, Snapshots & Subscriptions](/billing/plans-snapshots-subscriptions)
# How to Update Aikeedo
Source: https://docs.aikeedo.com/versioning/how-to-update
Stay up-to-date with the latest features and improvements by following our straightforward update instructions for each new version of Aikeedo.
Keeping your Aikeedo installation current ensures you benefit from the latest features, security patches, and performance enhancements. This guide provides a comprehensive, step-by-step process for updating Aikeedo, suitable for users of all technical levels.
**Upgrading to v5.1?** The update adds an `APP_ENCRYPTION_KEY` to your `.env`
file for [Connectors](/advanced/connectors#encryption-key) if there is none.
If the file is not writable, run `php bin/console app:generate-key` and back
the key up with your database. Private mode replaces private chats, so
re-enable it afterwards. See the [v5.1
notes](/versioning/release-notes#upgrading-to-v5-1).
**Upgrading to v5.0?** The standard update process on this page works, but
v5.0 removes tools and requires every plugin to be updated. Read the [v5.0
breaking changes and upgrade steps](/versioning/release-notes#upgrading-to-v5-0)
before you start.
**Upgrading from v2.x to v3.0?** This guide is **not** suitable for that
upgrade. You must follow the dedicated [Migration
Guide](/versioning/release-notes#migration-guide-upgrading-from-v2-x-to-v3-0)
instead. Attempting to use the standard update process for a v2.x → v3.0
upgrade may result in data loss or a broken installation.
## Before You Begin
Before initiating the update process, it's crucial to take a few preparatory steps:
1. **Check for Updates**: Visit our [official website](https://aikeedo.com) to confirm the latest available version of Aikeedo.
2. **Review the Changelog**: Familiarize yourself with the new features, improvements, and any potential breaking changes by reviewing our [changelog](https://aikeedo.com/changelog/?utm_source=docs\&utm_medium=content).
3. **Backup Your Data**: As a precautionary measure, create a complete backup of your Aikeedo data. This step is crucial to prevent any potential data loss during the update process.
Always perform updates in a staging environment first, if possible, to ensure
compatibility with your specific setup.
## Update Process
Follow these steps to update your Aikeedo installation:
1. **Download the Latest Version**:
* Obtain the latest Aikeedo package from the official source.
* Extract the downloaded zip file on your local machine.
2. **Locate the Update File**:
* Find the `aikeedo-server-files.zip` file within the extracted contents.
3. **Access the Admin Panel**:
* Log in to your Aikeedo admin panel.
* Navigate to the "Update" page in the admin dashboard.
4. **Upload the Update File**:
* Click on the "Choose File" button.
* Select the `aikeedo-server-files.zip` file you located earlier.
5. **Initiate the Update**:
* Click the "Install" button to begin the update process.
* Follow any on-screen instructions carefully.
The update process may take several minutes to complete. Do not close your
browser or interrupt the process until it's finished.
## Post-Update Steps
After the update process completes:
1. **Verify the Update**: Confirm that the update was successful by checking the version number in your admin panel.
2. **Clear Cache**: In most cases, this step is not necessary. Aikeedo automatically manages its cache during updates. However, if you notice any display issues, you may want to clear your browser cache as a troubleshooting step.
3. **Test Functionality**: Thoroughly test all aspects of your Aikeedo installation, including:
* User authentication
* AI-powered features
* Payment integrations (if applicable)
* Custom configurations or integrations
4. **Review Settings**: Some updates may introduce new settings or change existing ones. Review your configuration to ensure everything is set up as desired.
## Troubleshooting
If you encounter any issues during or after the update:
1. Review the update logs (if available) for any error messages.
2. Ensure all [server requirements](/overview/server-requirements) are still met after the update.
## Staying Informed
To stay updated on the latest Aikeedo releases and announcements:
* Follow us on [social media](https://x.com/heyaikeedo)
* Regularly check our [changelog](https://aikeedo.com/changelog/?utm_source=docs\&utm_medium=content)
By following these update procedures and staying informed, you'll ensure your Aikeedo installation remains secure, efficient, and equipped with the latest features to serve your users best.
# Release Notes
Source: https://docs.aikeedo.com/versioning/release-notes
Learn about the latest changes, breaking updates, and migration instructions for Aikeedo.
The full changelog is available at
[https://aikeedo.com/changelog/](https://aikeedo.com/changelog/?utm_source=docs\&utm_medium=content). This page
documents only breaking changes and critical migration instructions for major
releases.
## v5.1
v5.1 adds MCP connectors, an optional Connector Catalogue plugin, an app-wide private mode and keyboard shortcuts, and refreshes the model catalog.
**Back up your files and database before updating.** The standard update
process works and database migrations run automatically. After the update,
re-enable [Private mode](/advanced/private-mode) and update every installed
plugin.
### What's new in v5.1
* **Connectors (MCP)** — chat can use tools from remote MCP servers. Each user signs in with their own account, every tool has an always, ask or never permission (the default is to ask), and an approval card in the conversation shows exactly what a tool will send. Admins turn Connectors on under **Settings → Features → Chat** and choose per plan which connectors are included and how many custom ones a workspace may add. Tool calls are not charged credits. See [Connectors](/advanced/connectors).
* **Connector Catalogue plugin** — 487 vetted, ready-made MCP servers you import under **Settings → Starter content → Connectors**. Requires Aikeedo 5.1 or later and is available in the [marketplace](https://aikeedo.com/marketplace/connectors/?utm_source=docs\&utm_medium=content).
* **Private mode** — replaces private chats. One switch makes everything a user creates temporary across the whole app, and it stays on across pages and tabs in the same browser until turned off. See [Private Mode](/advanced/private-mode).
* **Keyboard shortcuts** — shortcuts for common actions, shown where they apply and listed in a cheat sheet. Users can switch each one off. See [Keyboard Shortcuts](/advanced/keyboard-shortcuts).
* **Model catalog refresh** — adds Claude Opus 5.5, Claude Sonnet 5.5, GPT 6.1 Sol, GPT 6 Luna, Grok 4.7 and Grok Imagine Video 1.5 Lite.
### Changes to be aware of in v5.1
#### Encryption key
Connectors store credentials encrypted, with the key in `APP_ENCRYPTION_KEY` in your `.env` file. The update writes a key if there is none. If `.env` is not writable, the admin **Connectors** page shows a warning: run `php bin/console app:generate-key`. **Back the key up with your database.** See [Connectors](/advanced/connectors#encryption-key).
#### Private mode replaces private chats
The chat-only temporary chat setting is removed. Turn on private mode under **Settings → Features → Privacy & safety**, set the retention period, and check your plans.
#### User-supplied URLs cannot reach private networks
Connector addresses, web pages read during chat and dataset URLs must be public. Addresses that point to `localhost`, private networks or cloud metadata endpoints are refused, including through redirects. An MCP server on the same machine or network as Aikeedo cannot be used as a connector.
#### Retired models removed
Claude Sonnet 4.5, GPT 5.1, GPT 5.4 nano, Command R and Command R+ are removed. Reassign any plans, assistants or defaults that used them.
### Upgrading to v5.1
Back up your files and database.
Follow the standard [How to Update](/versioning/how-to-update) process.
Database migrations run automatically.
Update all installed plugins from the marketplace.
* Re-enable private mode under **Settings → Features → Privacy & safety**
and check your plans. - Turn on **Connectors** under **Settings → Features →
Chat**, then choose what each plan includes. - Optionally install the
[Connector Catalogue](https://aikeedo.com/marketplace/connectors/?utm_source=docs\&utm_medium=content) plugin.
Confirm `APP_ENCRYPTION_KEY` is set in `.env` and backed up with your
database.
***
## v5.0
v5.0 is a major release. Chat gains Canvas, clarifying questions and media generation. The Writer becomes a guided wizard that continues in chat. Imagine and Video get templates, a community feed and a new composer. The interface is rebuilt around a Recents sidebar and a new Settings area. Every official plugin is now translated, and languages can be managed from the admin panel and completed with new translation tooling.
v5.0 removes tools and changes plugin interfaces. **Back up your files and
database before updating**, read the breaking changes below, and **update
every installed plugin** after the core update. The standard update process
works, and database migrations run automatically, with no manual SQL.
### What's new in v5.0
#### Chat
* **Canvas** — the assistant writes documents, code, HTML and SVG to a side panel instead of reprinting them in the thread. Revisions are patches, every version is kept and can be restored, and users can comment on a selected passage. All documents are collected in the Library under **Artifacts**.
* **Inline documents** — short standalone writing (emails, messages, posts, letters) renders as a card in the conversation. Emails can be opened in Gmail, Outlook or the default mail app.
* **Clarifying questions** — the model can pause to ask multiple-choice questions before answering an ambiguous request. The question turn and the answer turn are billed separately.
* **Media generation in chat** — the chat capability now generates images, video, voice-overs and transcripts, can edit an earlier image by reference, and shows results inline, including async generations. For voice-overs, users pick a voice right in the conversation and can listen to it first.
* **Personalization** — users can add their own chat instructions and tell the AI what to call them.
#### Writer
* **Preset wizard** — opening a Writer template asks its questions one screen at a time, then continues as a normal chat conversation with tools, Canvas and follow-ups. The filled prompt is never shown to the user.
* **Recommended model** — admins can set a recommended model on a template. See [Prompt Templates](/advanced/prompt-templates).
#### Imagine & Video
* **Templates** — admin-authored image and video templates with preview artwork, pinned settings, required input images and optional locking that hides the prompt and model. See [Prompt Templates](/advanced/prompt-templates#image-and-video-templates).
* **Community** — users can share generations to a feed visible to everyone on the installation (**Settings → Features → Community**).
* **New composer** — quick settings chips, `@image` reference tags, **Enhance** to rewrite a prompt and **Surprise me** to write one (**Settings → Features → Utilities**).
* **Inputs are kept** — reference images and frames are saved with each generation and shown in the details panel, and **Remix** restores them.
#### Interface
* **Recents** — each tool's history moves into the left sidebar, with rename, share, delete and read/unread status.
* **Head bar and Details panel** — every tool shows the open item's title, actions and a details panel with credits spent, the model used and other insights.
* **Settings area** — account, security, capabilities, API keys (now revocable), affiliates and workspace pages live under one Settings section with its own navigation.
* **Contextual "New" button**, a merged workspace and account menu, and redesigned dialogs that work properly on phones.
#### Localization and administration
* **Languages page** — enable languages, choose the default and see how complete each translation is from **Settings → Languages**. See [Localization](/advanced/localization).
* **Translation tooling** — `app:locale:extract`, `app:locale:translate` (AI translator) and `app:locale:build`. Updates now keep your own translation edits.
* **Starter content** — import Writer, image and video templates at any time from **Settings → Starter content** or during installation. See [Starter Content](/advanced/seed-data).
* **Model catalog refresh** — adds GPT 6 Astra, GPT 5.6 Sol/Terra/Luna, Opus 5, Fable 5.1, Command A+, Grok 4.6 and 4.5, GPT Image 2.5, Grok Image 2.0 and Video 1.5, Seedance 2.5, Nano Banana 2, Seedream 5.0 Pro, Kling 3.0 Turbo, Wan 3.0, Hailuo H3, Veo 3.1 Lite, Gemini Omni Flash and FLUX 3.
### Plugins
**All plugins are now translated.** Update each installed plugin to get its translations and v5.0 compatibility.
### Breaking changes in v5.0
#### Coder removed
The Coder tool is removed. Existing code documents are converted into chat conversations during the update, together with Writer documents.
#### Composer removed
The Composer (music) tool and its AI/ML API integration are removed. **Existing compositions and their files are deleted** during the update. Download anything you want to keep before updating.
#### Writer documents become conversations
The standalone Writer editor is replaced by the preset wizard. Existing Writer documents are converted into chat conversations during the update. The `writer.model` plan setting is no longer used; users pick the model in the wizard.
#### `POST /api/ai/completions` removed
The legacy completions endpoint is removed from the User API. Use the conversations API instead.
#### `TitleServiceInterface` removed
`Ai\Domain\Title\TitleServiceInterface` and the provider `TitleGeneratorService` classes are removed. Plugins that resolved them must use `TextCompletionServiceInterface` or the `Titler` service instead. This is why every plugin needs updating.
#### Image generation capability renamed
The `generate_image` plan tool is replaced by `generate_media`, which covers images, video, voice-over and transcription. Review the chat capabilities on each plan after updating.
#### Title generator model is global
The title generator model is no longer set per plan. Set it once in **Settings → Features → Utilities**.
#### Reference image uploads narrowed
Reference images for Imagine and Video accept JPEG, PNG, WebP and GIF only.
#### Retired models removed
o1, o3, o3-mini, o3-pro, o4-mini, GPT-4, GPT-4 Turbo, GPT-4.1 nano, GPT-3.5 Turbo, Sora 2, Sora 2 Pro, GPT Image 1, 1 mini and 1.5, Claude Sonnet 4.0, Imagen 3 and 4, Veo 2 and 3, FLUX.1 Pro and Lucy are removed. Reassign any plans, assistants or defaults that used them.
#### Starter content re-import
Writer templates now ship with fixed IDs. If you imported the built-in templates before v5.0 and import them again from **Settings → Starter content**, you get one set of duplicates to delete. New installations are unaffected.
### Upgrading to v5.0
Back up your files and database. If you use Composer, download the
compositions you want to keep.
Follow the standard [How to Update](/versioning/how-to-update) process.
Database migrations run automatically.
Update all installed plugins from the marketplace so they are compatible
with v5.0 and ship their translations.
* Enable your languages in **Settings → Languages**. - Set the title
generator and prompt tools model in **Settings → Features → Utilities**. -
Turn on Canvas, Clarifying questions and Media generation in **Settings →
Features → Chat**, and enable them on your plans. - Optionally enable
**Community** and import templates from **Settings → Starter content**.
Make sure `/{webroot}/locale` (for example `public/locale`) is writable, so
the browser translation dictionaries can be rebuilt.
***
## v4.1
v4.1 adds operator-facing billing and chat controls, plus self-service account management.
v4.1 has no breaking changes and requires no migration. You can update using
the standard process — see the [How to Update](/versioning/how-to-update)
guide.
### What's new in v4.1
* **Session-based usage mode** — optional alternative to credit metering that paces usage through configurable session windows while hiding credit balances from users. See [Billing Overview](/billing/overview#usage-mode).
* **Configurable detailed pricing page URL** — set where the Plans page "Detailed pricing table" button links, including external marketing sites.
* **Self-service account deletion** — allow users to permanently delete their own accounts (admin toggle). See [Account Settings](/authentication/accounts-settings#account-deletion).
* **Temporary chat** — let users start chats that are not saved to history, with a configurable retention period.
* **Chat thinking / reasoning controls** — configure per-model reasoning profiles and expose thinking on/off and effort controls in chat.
***
## v4.0
v4.0 is the largest Aikeedo release to date. It introduces the Chatbots plugin, a full Tax Layer, new payment gateways, the Sources module, and a range of platform improvements.
v4.0 has no breaking changes and requires no migration. You can update using
the standard process — see the [How to Update](/versioning/how-to-update)
guide.
***
## v3.0
Please review these notes carefully if you are upgrading from v2.x.
Version 3.0 introduced several breaking changes. If you are upgrading from
v2.x, follow the migration guide below.
### Breaking Changes in v3.0
#### Plugins Architecture Changed
* Plugins are now installed in `/extra/extensions` instead of `/public`.
* Old plugins remain compatible, but you must reinstall them manually.
#### "Cohere Web Search" Chat Capability Removed
* The Cohere integration now uses Cohere API v2.0.
* The built-in web search feature from Cohere has been removed and replaced with Google Search.
* Please update your plan configuration accordingly.
#### Removed Models
The following legacy/deprecated models have been removed. Please use other available models:
* openai/gpt-3.5-turbo-instruct
* openai/o1-preview
* openai/o1-mini
* openai/gpt-4.5-preview
* openai/text-moderation-latest
* openai/dall-e-2
* fal-ai/flux-realism (not available anymore)
* grok-beta
* grok-vision-beta
#### Search Box Removed
* The search box on the user app dashboard has been replaced with a new quick access bar.
* To search for library items, visit the Library pages.
#### Ai/Infrastruture Namespace Renamed
* The namespace is now correctly spelled as `Ai/Infrastructure`.
* If you use services from this layer in custom code, update the namespace accordingly.
#### Change in ImageServiceInterface::generateImage() Method Signature
* The signature of `Ai\Domain\Image\ImageServiceInterface::generateImage()` has changed.
* It now returns `ImageEntity` directly.
* Update any custom image generator services to match the new signature.
#### Keys for the Following Models Have Been Updated
If you use any of the following models, set the credit rates and enable them both globally and at the plan level again:
* fal-ai/flux/dev
* fal-ai/flux/schnell
* fal-ai/flux-pro
***
### Migration Guide: Upgrading from v2.x to v3.0
Version 3.0 is a major release with breaking changes. A standard update is NOT
sufficient for upgrading from v2.x to v3.0. Please follow this migration guide
to avoid data loss.
Before starting the migration, create a full backup of your database and all files in your current Aikeedo installation.
This is essential to prevent data loss. Make sure your backup includes user uploads and your database.
Delete all files and folders from your installation directory.
This will remove all application files. Do not proceed unless you have a
backup.
Before installing the new version, ensure you have your previous database name
and user credentials ready. You will need to enter these during installation
to preserve your data. When prompted by the installer, choose the
Migrate option to keep your current data.
If you provide the same database details as your previous installation, the
installer will ask whether you want to migrate your current data or perform
a fresh install.
Choose the Migrate option
to preserve your data. Only choose a fresh install if you want to start with
a clean database.
Install the new version in the same directory by extracting to the same
location as your previous installation. Then follow the
installation guide to complete the setup.
The new version package includes all required files and directories.
Overwrite any existing files if prompted.
After installation is complete, restore your /public/uploads
directory from your backup.
If you are using public\_html instead of public,
restore your uploaded files from your backup to
/public\_html/uploads.
Navigate to the Update page in the Admin panel. This step is crucial as
it finalizes the update process in the background.
Do not skip this step. The update will not be fully applied until you visit
the update page in Admin.
Install your plugins again from the admin UI.
Plugin architecture has changed in v3.0. All plugins must be reinstalled manually.
If you have custom plugins or integrations, review the breaking changes above and update your code as needed.
# Branding
Source: https://docs.aikeedo.com/website-basics/branding
Customize your Aikeedo platform with custom logos, favicons, color schemes, and settings to create a unique brand identity.
Aikeedo provides comprehensive branding options to customize the visual identity of your platform. This guide covers logo configuration, color schemes, and appearance settings.
## Accessing Branding Settings
To access the branding settings:
1. Log in to your Aikeedo admin panel
2. Navigate to the "Settings" section in the left sidebar
3. Under the "Branding" category, you'll find options for "Logo" and "Appearance"
The branding settings affect both the admin panel and the user-facing parts of your Aikeedo instance.
## Logo Settings
Your logo is often the first thing users notice about your brand. Aikeedo allows you to customize various logo elements:
### Favicon
The favicon is the small icon that appears in browser tabs and bookmarks.
* Click on "Browse files" to upload your favicon
* Supported formats: PNG, JPG, JPEG, GIF, ICO, or SVG
* Recommended size: 16x16 pixels or 32x32 pixels for better quality on high-resolution displays
### Primary Logo
The primary logo is used in the header of your website and emails.
* Upload separate logos for light and dark modes
* Supported formats: PNG, JPG, JPEG, GIF, ICO, or SVG
* Recommended size: Varies depending on your design, but ensure it's clear and legible
### Alternative Logo
The alternative logo is typically used in the footer of your website and emails.
* Upload separate logos for light and dark modes
* Supported formats: PNG, JPG, JPEG, GIF, ICO, or SVG
* Recommended size: Usually smaller than the primary logo
For the best results, use SVG format for your logos. SVGs are scalable and will look crisp on all devices and screen sizes.
## Appearance Settings
Customize the color scheme and overall look of your Aikeedo instance:
### Color Schemes
* **Light mode**: Enable or disable light mode for your users
* **Dark mode**: Enable or disable dark mode for your users
* **Default color scheme**: Choose between "System" (follows user's device settings) or set a specific default
### Accent Colors
* **Accent color**: The primary brand color used for buttons, links, and highlights
* **Accent text color**: The color of text that appears on elements using the accent color
These settings apply to both the admin panel and user interface. However, they may not affect the landing page, depending on your chosen theme.
## Best Practices
Follow these guidelines to create a professional brand identity:
1. **Consistency**: Maintain consistent branding across all touchpoints including website, emails, and notifications
2. **Readability**: Choose colors with sufficient contrast to ensure text remains easily readable
3. **File Optimization**: Compress image files to ensure fast loading without sacrificing quality
4. **SVG Format**: Use SVG format for logos when possible for scalability across all devices
5. **Testing**: Preview your branding in both light and dark modes before finalizing
Test your color choices with accessibility tools to ensure they meet WCAG contrast requirements for all users.
## Related Guides
* [General Settings](/website-basics/essentials) - Configure site name and SEO
* [Landing Page Customization](/website-basics/landing-page) - Customize landing page theme
* [PWA Settings](/website-basics/pwa) - Set up progressive web app features
* [Application Configuration](/setup/configuration) - Complete setup checklist
# General Settings
Source: https://docs.aikeedo.com/website-basics/essentials
Configure website name, SEO metadata, keywords, and landing page settings in Aikeedo to optimize your platform for search engines and user experience.
Navigate to **Settings > General** in your Aikeedo admin panel to configure your platform's core website information and SEO settings.
## Website Information
### Website Name
The website name appears throughout your platform and represents your brand.
* Locate the "Name" field under the "Website" section
* Enter your brand name or platform title
* Example: "AI Wizard" or "AI Wizard Platform"
Choose a memorable name that clearly communicates your platform's purpose to users.
### Site Domain
Configure your website's domain for proper functionality.
* Enter your domain name in the "Site Domain" field
* **Important:** Include only domain, subdomain (if any) and port number (if any)
* **Do not include** protocol (http/https) or path
* Omit port number if it's 80 or 443
* Example: `demo.aikeedo.com` or `mysite.com:8080`
Incorrect domain configuration can cause authentication and payment gateway issues. Make sure to enter only the domain without http\:// or https\://.
### SSL Configuration
Enable SSL for secure connections and required features.
* Toggle "Is SSL Enabled?" to "Yes" if you have an SSL certificate installed
* SSL is required for various features like SSO and Payment Gateways
* It's highly recommended to enable SSL for production environments
Only enable SSL if you have a valid SSL certificate installed on your server. Enabling without a certificate will cause connection errors.
### Landing Page
Control the first page visitors see when they arrive at your site.
* Enable this toggle to display the default landing page
* When disabled, visitors are redirected to the login page
If you plan to publicly market your platform, keep the landing page enabled to showcase your services to potential users.
### Default App Page
Set the default page users see after logging in.
* Enter the path for your default app page (e.g., `/app`)
* This setting will be used as the default page for users when they login
* Admins will always be redirected to the admin dashboard regardless of this setting
Common values include `/app`, `/app/chat`, or `/app/video` depending on your platform's structure.
## SEO Optimization
Search Engine Optimization (SEO) is vital for improving your site's visibility in search results.
### Keywords
1. In the "SEO" section, locate the "Keywords" field.
2. Enter relevant keywords for your AI service, separated by commas.
3. Example: "AI assistant, machine learning, natural language processing, chatbot"
Choose keywords that accurately describe your service and that potential users might search for. Don't overstuff with keywords; focus on the most relevant ones.
### Description
1. Find the "Description" field in the SEO section.
2. Write a concise, compelling description of your AI service (typically 150-160 characters).
3. This description may appear in search engine results, so make it informative and engaging.
## Saving Changes
After configuring your general settings:
1. Review all changes for accuracy
2. Click the "Save changes" button at the bottom of the page
3. Wait for the confirmation message
Your general settings have been updated successfully.
## Related Guides
* [Landing Page Customization](/website-basics/landing-page) - Customize landing page theme
* [Branding](/website-basics/branding) - Customize logos and colors
* [Application Configuration](/setup/configuration) - Complete configuration checklist
* [Email Configuration](/email/mailer) - Set up email delivery
# Landing Page Customization
Source: https://docs.aikeedo.com/website-basics/landing-page
Customize your Aikeedo landing page including hero images, text content, and theme files while preserving changes through updates.
## Overview
The landing page is the first impression visitors get of your Aikeedo platform. This guide covers how to customize images, text, and theme files safely.
Changes made directly to the default theme will be overwritten during app updates. Always duplicate the theme before making customizations.
**Directory Note**: If you've replaced the default `public` directory with `public_html` (common in cPanel environments), replace `public` with `public_html` in all file paths throughout this guide.
## Landing Page Options
Aikeedo offers flexibility in how you manage your landing page:
Use Aikeedo's built-in landing page and customize it to match your brand. This option gives you full control while staying within the Aikeedo ecosystem.
**Best for:** Users who want to customize the existing design without external tools.
Install Aikeedo on a subdomain (`app.yourdomain.com`) and disable the default landing page. Create your main landing page using external services like Framer, WordPress, Webflow, or Squarespace, then point your main domain (`yourdomain.com`) to your third-party landing page.
**How it works:** Main domain shows your custom landing page, while the subdomain handles Aikeedo functionality (authorized users go to dashboard, unauthorized visitors see login page).
**Best for:** Users who want complete design freedom with specialized landing page builders.
Set the **Detailed pricing page URL** in [Billing settings](/billing/overview#detailed-pricing-page-url) so the in-app Plans page button links to your external pricing page instead of the default `/#pricing` anchor.
Browse and install pre-built themes from our [Marketplace](https://aikeedo.com/marketplace/?utm_source=docs\&utm_medium=content). These professionally designed themes are ready to use and can be customized to match your brand.
**Best for:** Users who want professional designs without starting from scratch.
Create custom themes tailored to your specific needs. You can build themes yourself or have our team create them for you.
**For developers:**
* Start with our minimal [Theme Starter Kit](https://github.com/heyaikeedo/themes-starter) - the same foundation we use internally
* Follow our comprehensive [Theme Development Guide](/development/themes/overview) for detailed instructions
**For custom development:**
* Contact [sales@aikeedo.com](mailto:sales@aikeedo.com) to have our team create custom themes for you
**Best for:** Users who need unique designs or have specific branding requirements.
## Customize the Default Landing Page
To customize Aikeedo's default landing page, you'll need to duplicate the theme first to protect your changes from being overwritten during updates.
Copy the default theme to a new location:
```bash theme={null}
# From
/extra/extensions/heyaikeedo/default
# To
/extra/extensions/yourorganization/themename
```
Replace `yourorganization` and `themename` with your preferred names.
In your duplicated theme directory, update the `composer.json` file to reflect the new theme name:
```json theme={null}
{
"name": "yourorganization/themename"
}
```
The name must match the new directory structure.
Copy the theme assets to match your new theme:
```bash theme={null}
# From
/public/e/heyaikeedo/default
# To
/public/e/yourorganization/themename
```
If using `public_html`, replace `public` with `public_html` in the paths above.
1. Navigate to the admin panel
2. Go to **Themes** page
3. Find your new theme in the list
4. Click **Activate** to enable it
For development, you need to enable debug mode and disable app cache:
1. Navigate to the **Status** page in your admin panel
2. **Enable Debug Mode**: This allows you to see changes in real-time during development
3. **Disable App Cache**: Disable all caching options on the same page
Debug mode and disabled cache should never be enabled in production as they significantly impact performance.
Edit files in your custom theme directory:
* **Modify TWIG templates**: Aikeedo uses TWIG for HTML views (`.twig` files)
* Update CSS styles
* Add custom JavaScript
* Replace images
TWIG templates are located in the `views` directory of your theme. Look for `.twig` files to modify the HTML structure.
Once you've completed and tested your changes:
1. Navigate to the **Status** page in your admin panel
2. **Disable Debug Mode** for optimal performance
3. **Re-enable App Cache** for production
If you have any caching enabled, clear the application cache from the admin panel's Status page after making changes.
Your customizations are now protected from being overwritten during updates.
## Theme File Locations
Understanding where theme files are located helps you customize effectively:
### Asset Files (CSS, JS, Images)
```
/public/e/heyaikeedo/default/
```
Contains all frontend assets including:
* Stylesheets (CSS)
* JavaScript files
* Images and icons
### HTML View Files
```
/extra/extensions/heyaikeedo/default/
```
Contains template files that generate the HTML structure.
## Quick Customizations
### Updating the Hero Image
The hero image is located at:
```
/public/e/heyaikeedo/default/assets/preview.webp
```
To update the hero image:
1. Replace the existing `preview.webp` file with your new image
2. Keep the same filename (`preview.webp`)
3. Clear your browser cache to see the changes
For best results, use WebP format for optimal performance and loading speed.
### Updating Text Content
Text content should be updated through localization files for proper multilingual support.
Localization files are located at:
```
/extra/extensions/heyaikeedo/default/locale
```
See the [Localization guide](/advanced/localization) for detailed instructions on managing text translations.
## Advanced Theme Development
For comprehensive theme customization and development:
### Default Theme Source Code
Access the default theme source code and documentation:
View the complete source code, structure, and implementation of the default landing page theme
### Theme Development Resources
For in-depth information on building custom themes:
Learn about theme architecture, available objects, and best practices for theme development
Minimal foundation for building custom themes from scratch - the same base we use internally
## Best Practices
Follow these guidelines for successful landing page customization:
1. **Always Duplicate**: Never edit the default theme directly
2. **Version Control**: Keep your custom theme under version control (Git)
3. **Test Thoroughly**: Test changes across different browsers and devices
4. **Optimize Assets**: Compress images and minify CSS/JS for better performance
5. **Document Changes**: Keep notes on customizations for future reference
6. **Regular Backups**: Backup your custom theme before major changes
If you only need to change colors and logos, consider using the [Branding settings](/website-basics/branding) instead of creating a custom theme.
## Troubleshooting
Common issues and solutions:
* Clear your browser cache (hard refresh: Ctrl+Shift+R or Cmd+Shift+R)
* Clear the application cache from the admin panel
* Verify you're editing the active theme, not the default theme
* Verify the `composer.json` file has the correct name format
* Ensure the directory structure matches the name in `composer.json`
* Check file permissions on the theme directories
* Verify asset files are in the correct `/public/e/` directory
* Clear browser cache and application cache
* Check browser console for 404 errors on asset files
## Related Guides
* [Branding](/website-basics/branding) - Configure logos and colors
* [General Settings](/website-basics/essentials) - Set site name and SEO
* [Localization](/advanced/localization) - Manage translations
* [Theme Development Guide](/development/themes/overview) - Advanced theme development
Remember to disable debug mode on production sites to ensure optimal performance and security.
# Progressive Web App (PWA)
Source: https://docs.aikeedo.com/website-basics/pwa
Configure Aikeedo as a Progressive Web App (PWA) with custom icons, theme colors, and offline capabilities for an app-like user experience.
Progressive Web Apps (PWAs) combine the best of web and mobile applications, offering fast, reliable, and engaging experiences. This guide covers PWA configuration for your Aikeedo platform.
## What is a PWA?
A Progressive Web App is a type of application software delivered through the web, built using common web technologies including HTML, CSS, and JavaScript. PWAs are designed to work on any platform that uses a standards-compliant browser, including both desktop and mobile devices.
PWAs can offer features like offline functionality, push notifications, and home screen installation, providing a native app-like experience to your users.
## Configuring Your PWA
To set up your Aikeedo application as a PWA, follow these steps:
### 1. Enable PWA Functionality
First, ensure that the PWA feature is enabled for your application:
1. Navigate to the PWA settings page in your Aikeedo admin panel.
2. Locate the "Status" toggle switch.
3. Ensure it is set to "Enabled" (the switch should be green).
### 2. Basic Configuration
Fill in the following fields to customize your PWA:
* **Name**: Enter the full name of your application (e.g., "Aikeedo").
* **Short Name**: Provide a shorter version of your app name, which will be used when space is limited (e.g., on device home screens).
* **Display**: Choose how you want your app to appear when launched. The "Minimal" option provides a streamlined user interface.
* **Description**: Write a brief description of your application (e.g., "All-in-one AI tool").
### 3. Visual Customization
Customize the look of your PWA with these settings:
* **Theme Color**: Enter a hexadecimal color code (e.g., "#ffffff") to set the primary theme color of your PWA. This color may be used in the browser's address bar or other UI elements.
* **Background Color**: Specify a hexadecimal color code (e.g., "#3F4246") for the background color of the splash screen that appears when your PWA is launched.
### 4. App Icon
The app icon is crucial for brand recognition and will be displayed on the user's home screen or app launcher:
1. Click the "Browse files" button next to "App icon".
2. Upload an image that meets the following criteria:
* Recommended size: 512x512 pixels
* Supported formats: PNG, SVG, or WEBP
* Ensure the icon is visually appealing and recognizable at smaller sizes
### 5. Maskable Icon
Maskable icons adapt to different device requirements by allowing the icon to fill the entire shape provided by the device's operating system:
1. Toggle the "Maskable icon" switch to "Enabled" if you want your icon to be maskable.
2. Ensure your uploaded icon has some padding around the main content to accommodate different mask shapes.
### 6. Save Your Changes
After configuring all the settings, click the "Save changes" button at the bottom of the page to apply your PWA configuration.
## Testing Your PWA
After configuring PWA settings, test the installation:
1. Visit your site on a mobile device or Chrome on desktop
2. Look for the "Install" or "Add to Home Screen" prompt
3. Install the PWA and verify the icon, name, and colors appear correctly
## Related Guides
* [Landing Page Customization](/website-basics/landing-page) - Customize landing page theme
* [Branding](/website-basics/branding) - Customize logos and colors
* [General Settings](/website-basics/essentials) - Configure site name and SEO
* [Application Configuration](/setup/configuration) - Complete setup checklist
PWA features enhance user engagement by providing an app-like experience directly from the browser, increasing accessibility across all devices.