Developers
Modding the server
Extend a Tyto server with your own composer package — a standard Symfony bundle, installed into a derived image, using stock override mechanics.
Before reaching for code: a lot of “modding” needs no fork and no bundle. Outbound webhooks push events to your own service, and bots with API keys can read and write through the full REST API. Reach for a server-side mod only when you need to change how the server itself behaves.
Core is a stock Symfony 7.4 application, so the entire standard Symfony extension toolbox applies: ship your mod as a composer package containing a bundle, install it, and override what you need.
Package skeleton
A minimal mod package:
acme/tyto-mod/
├── composer.json
└── src/
├── AcmeTytoModBundle.php
└── EventSubscriber/…
{
"name": "acme/tyto-mod",
"type": "symfony-bundle",
"require": { "php": ">=8.5" },
"autoload": { "psr-4": { "Acme\\TytoMod\\": "src/" } }
}
type: symfony-bundle is what Symfony Flex looks for when registering a
bundle — in the derived-image build below Flex doesn’t run, so you
register it by hand. Services inside the bundle use the normal autowiring
you’d write in any Symfony app.
Installing into the server
Production servers run the prebuilt image, so a mod is installed by
deriving your own image. The runtime image ships no Composer binary and
its autoloader is built --classmap-authoritative, so resolve the
dependency in a Composer stage and copy the result back in:
ARG TYTO_VERSION=1.0.0-beta.3
# Stage 1 — add the mod to the shipped vendor tree
FROM composer:2 AS mod
ARG TYTO_VERSION
COPY --from=ghcr.io/tyto-chat/tyto-core:${TYTO_VERSION} /app /app
WORKDIR /app
RUN composer require acme/tyto-mod --no-scripts --no-interaction \
&& composer dump-autoload --no-dev --optimize --classmap-authoritative
# Stage 2 — the image you run
FROM ghcr.io/tyto-chat/tyto-core:${TYTO_VERSION}
COPY --from=mod /app/vendor /app/vendor
COPY --from=mod /app/composer.json /app/composer.lock /app/
COPY bundles.php /app/config/bundles.php
--no-scripts means Symfony Flex does not run, so the bundle is not
auto-registered. Copy the image’s config/bundles.php, append your
bundle, and ship that file (the COPY above):
return [
// …every line from the stock file…
Acme\TytoMod\AcmeTytoModBundle::class => ['all' => true],
];
Point compose.yaml at your image (or add a build: override) and keep
the version pinned — rebuild when you bump it. The entrypoint’s cache
warmup on boot picks the new bundle up; no other wiring needed.
What you can override
All stock Symfony mechanics — nothing Tyto-specific to learn:
-
Decorate a service — the main extension point. Public surfaces are interfaces (
VoiceServiceInterface,SearchServiceInterface,WebPushSenderInterface, …), so#[AsDecorator]wraps or replaces any of them:#[AsDecorator(decorates: WebPushSenderInterface::class)] final class LoggingPushSender implements WebPushSenderInterface { … } -
Listen to events — kernel events, Doctrine lifecycle events and messenger middleware work as documented by Symfony; register plain
#[AsEventListener]subscribers from your bundle. -
Add endpoints — your bundle can ship its own API Platform resources or controllers under your namespace; they mount alongside the core API.
-
Add entities — bundle-owned Doctrine entities with your own migration namespace keep your schema separate from core’s.
-
Override translations — the application’s
translations/directory wins over bundle catalogs, and your bundle can add new domains.
Ground rules
Only the HTTP API is versioned. Service classes, entities and internal interfaces can change between releases without notice — pin the exact core version your mod targets, and re-test on every bump before rolling it out.
- Don’t patch core files in the derived image — everything above works without touching them, and vendor patches evaporate on upgrade.
- Keep write access honest: go through the domain services (which enforce authorization and invariants), never straight to the EntityManager — the same code guidelines that apply in core are what keep a mod from corrupting state.
- A mod that would be useful to everyone is a PR waiting to happen — see Contributing.