First check whether anything needs building
The first look goes into the core, and it pays off more often than people expect. Out of the box, TYPO3 provides forms through the Form Framework, redirect management, categories, multilingual sites through the site configuration, a full-text search and the Scheduler for recurring tasks. Since v13, site sets bundle TypoScript, settings and dependencies into packages that can be activated per site. Much of what used to justify a small extension is now a few lines of configuration or an adjusted Fluid template. So before we write an extension, we check whether TypoScript, a site set or the core already solve the task. An extension that is never installed costs nothing — neither to build nor at the next update.
The second route is a maintained extension from the TER or Packagist. For news, calendars, maps, cookie consent or search integration there are established solutions, and building your own alongside them would be a waste. “Maintained” means something specific: a release for the current LTS, a maintainer who responds to bug reports, a public issue tracker and a record of security fixes that arrived promptly. It also matters how an extension handles new major versions: from version 14.0.0, the news extension no longer supports TYPO3 v12 — anyone who wants to keep it current has to move with it. If an author does not deliver a fix for a security issue, the TYPO3 Security Team removes the affected versions from the TER. If a maintained extension covers your requirement, we recommend it, set it up and include it in maintenance rather than building the same function a second time.
The third route is custom development. It delivers exactly the function your process needs, without configuration screens for cases you do not have and without somebody else's roadmap. In return, maintenance is part of the deal from day one. Every extension, downloaded or built, is an item in your running costs: every core patch update has to check it, every major upgrade requires it to be adapted, every security notice about it has to be assessed. That is why we compare the routes over the years you intend to run the website, not over the quoted price. And some requirements are not extension requirements at all but an editorial rule nobody has written down yet. Those get written down, not programmed.
How we build an extension
We build an extension as a Composer package with its own vendor namespace and a composer.json that states the type typo3-cms-extension, the extension key and the supported TYPO3 versions. From v14 onwards a valid composer.json is mandatory anyway, including in classic-mode installations. Project-specific extensions live in the project repository, reusable ones in a repository of their own; you receive the source code in a Git repository that belongs to you. Configuration lives in files and therefore in version control: TCA for tables and fields, a site set for TypoScript and settings, Symfony dependency injection for the services. What editors may change sits in the backend. What is code sits in the repository — and nowhere else.
We use Extbase and Fluid where they fit: records with list and detail views, forms with validation, a domain model that is more than a single table. For a JSON endpoint, a teaser fed from an external API or a small piece of output, Extbase is overhead; a lean controller or a PSR-15 middleware is enough. We build custom content elements with Content Blocks — an extension, not part of the core — or with our own TCA definitions and a Fluid template, so editors can see what they are maintaining. At Eschen Nutzfahrzeuge (used trucks and commercial vehicles), the vehicle stock is set up as structured records, maintained by the client's own editors and usable on mobile. Structured records can be filtered, sorted and reused elsewhere; running text cannot. Anything that fits into no content element — an import overview, an approval list — gets its own backend module.
We hook in through the extension points TYPO3 provides. PSR-14 events to attach to processes in the core and in other extensions. PSR-15 middlewares for anything that happens to the request or the response, such as a custom API endpoint. Symfony dependency injection to configure services rather than creating them with new. TCA overrides to add fields to existing tables. And the DataHandler whenever records are written programmatically — then permissions, logging, the reference index and the cache work just as they do for input in the backend. What does not belong: core patches, which the next Composer update overwrites, and XCLASS, which replaces a core class entirely; only one XCLASS per class takes effect, and every change to the original has to be followed by hand. Whatever hooks in through an intended extension point generally survives a major release. Whatever goes around it has an expiry date.
Interface extensions: data from other systems
Many extensions have a single job: bringing data from another system onto the website. At Autohaus Brüggemann, a car dealership with six locations from Rheine to Rostock, eight franchised brands and stock in the four-digit range, the TYPO3 website brings all vehicles together in one shared search. It is fed by the dealer management system, not by the editorial team. That is the pattern: whatever another system already holds is not maintained a second time by hand. The website shows the same state as the system behind it, and nobody retypes vehicle data. For that to hold, it has to be settled beforehand which data flows in which direction, how often, and what happens when something fails.
Technically, we run an import as a Symfony Console command, callable via vendor/bin/typo3 and scheduled through the Scheduler. That means it can be triggered by hand, tested and traced on the command line instead of running inside a frontend request that times out on large data volumes. Every run writes a log: how many records were created, changed or deleted, which were rejected and why. An interrupted run can be repeated without damage, because each record is matched by a unique key from the source system rather than by its position in a file. Data is written through the DataHandler or into a clearly separated table of its own — never by direct access to core tables that bypasses permissions and caching.
The decisive question is not a technical one: which system owns which field? If price and availability come from the source system, editors must not overwrite them — otherwise either the website is wrong after the next import or the editors' work is lost. Additional texts, images or highlights, on the other hand, stay editorial, in separate fields the import never touches. We put this ownership per field in writing before the first line of code. We also draw one boundary deliberately: a shop does not belong in the CMS as an extension. At M&S Armaturen (industrial valves), the corporate website runs on TYPO3 and the Shopware shop is connected to it. A sales process with pricing rules, payment methods and returns belongs in a shop system, and we build that alongside with Shopware 6 instead of forcing it into a CMS extension.
Third-party and orphaned extensions in an upgrade
In a major upgrade, the brake is rarely the core and usually the extension list. That is why every upgrade we carry out starts with an inventory: which extensions are installed, which of them are actually used, who maintains them, is there a release for the target version? Whether an extension is used is shown by the database, not by anybody's memory — a plugin that is no longer placed on any page does not need migrating; it needs removing. In the end every extension gets a note: update, replace with the core, replace with a maintained extension, port, rebuild or remove. This list is usually shorter than feared, and it is the basis of any honest estimate.
For an extension without a release for the target version there are three routes. Replacement with the core or a maintained alternative, even if configuration and data have to move in the process. Porting, meaning taking the code into your own repository and adapting it to the new version — the GPL under which TYPO3 extensions are published allows this, but with the fork comes the maintenance. Or rebuilding the function, often with a smaller scope, because it turns out that half the options were never used. A rebuild has an advantage that is often overlooked: what the function has to do can be read off the running system. Both figures, porting and rebuild, sit side by side in the estimate, even when one of them is the smaller job.
For the porting itself there are tools that take part of the work off your hands. The Extension Scanner in the Install Tool finds calls to API that has been removed or deprecated in the target version. TYPO3 Rector rewrites part of those places automatically, and Fractor does the same for TypoScript, FlexForms, YAML and Fluid. What remains is manual work: business logic in templates, dependencies with conflicting Composer constraints and, in v14 above all, switching plugin registration from list_type to dedicated CTypes, including an upgrade wizard so that existing content elements keep rendering after the upgrade. Which breaking changes v14 brings in detail, and how an upgrade project runs, is covered on our TYPO3 update page. Here the subject is the code that has to move along with it.
Taking over somebody else's custom extensions
We take over custom extensions built by a previous agency, but not blindly. Before we commit, there is a code review with the same set of questions every time. Is there a composer.json, and which TYPO3 version is it tied to? Does the code live in a repository, or only on the server? Does the extension hook in through events and dependency injection, or through XCLASS, outdated hooks and direct access to core tables? Does it write records through the DataHandler? Is business logic sitting in Fluid templates or in TypoScript? What does the Extension Scanner find against the target version? Do tests exist, and do they pass? This is work of hours to days, and a defined first deliverable with its own price, which we state in writing beforehand.
The result is a findings report: what the extension does, where it reaches into core internals and what needs doing for the next version, with a prioritised risk list and the effort in person-days. Next to it sits the alternative of a rebuild with its own figure, so you can choose between the two routes before money goes into the code. And we decline if an extension is built in a way that means we cannot vouch for its behaviour after the next update, or if central parts were delivered without source code — then a rebuild is the more honest job. Taking over somebody else's code without reading it first mostly means taking over its surprises.
After the takeover comes order first, then maintenance. The code moves into a Git repository that belongs to you and gets a composer.json, version numbers following semantic versioning and documentation explaining what the extension does and how it is configured. Critical processes get tests with the TYPO3 Testing Framework before anyone changes them — otherwise nobody knows whether a change broke something. Only then do we port or extend. Ongoing maintenance runs under our TYPO3 maintenance with a service level agreement, and if you wish together with hosting on our own servers in the Hetzner data centre in Falkenstein, Germany. Like TYPO3 itself, the extension is licensed under the GPL; the source code belongs to you, and the domain is registered in your name.