Autonomy for Teams, Office on desktop, a fresh take on Photos
Explore what’s new in the release blog!
To build a Nextcloud app that works with different databases, thousands of users, and unpredictable server configurations, it’s essential to understand why Nextcloud is built the way it is and where developers most often get tripped up when their code leaves their local test instance.
In this article, you’ll learn how a request actually moves through Nextcloud’s architecture, how apps are meant to talk to each other (and why they shouldn’t talk to each other directly), and where the line between public and private APIs really sits. You’ll understand how Nextcloud models and reacts to data like entities, migrations, and indices, and how the event system lets apps stay loosely coupled from one another. Finally, you’ll learn more about the mistakes that only show up at scale and what it takes to get an app certified and published to the Nextcloud App Store.
The most important thing to keep in mind: The most durable Nextcloud apps are the ones built with the platform’s structure in mind, not around it.
Nextcloud under the hood
There is some institutional knowledge that experienced Nextcloud developers take for granted and that can easily trip up newcomers. Here’s what you need to keep in mind before you start developing your first Nextcloud app.
How Nextcloud code actually runs
Before writing any code, it helps to know where the boundaries actually are, specifically what’s public and stable, what’s internal and liable to change without warning, and how core and shipped apps relate to one another.
Public vs. private APIs
Nextcloud draws a hard line between its public and private APIs. Core functionality sits behind interfaces, and developers are expected to code against those interfaces rather than concrete implementations. Importantly, the apps Nextcloud ships itself (e.g. Talk or Calendar) use the exact same public APIs that third-party developers have access to. There’s no special internal-only shortcut. If it works for Nextcloud’s own teams, it’s built on the same contract available to everyone else.
A key design principle is that apps don’t call each other’s code directly. Instead, they listen to events and react independently. This keeps apps from depending on one another being installed, which matters a lot in an ecosystem where users pick and choose which apps to enable.
Core vs. shipped apps
At the core level, Nextcloud provides the platform itself: authentication, file storage, sharing, database abstractions, and routing. On top of that, a set of apps ship alongside the server but live in their own repositories, such as DAV (the abstraction layer for WebDAV, the protocol Nextcloud uses to read and write files safely, even when multiple people touch the same file at once), CalDAV and CardDAV, which extend that same idea to calendars and contacts. Administrative features like the settings panel are also built as their own apps, giving every app (including third-party ones) a consistent way to plug into the settings UI or the left sidebar.
The contract between core and apps is called OCP (the public, stable API surface). Breaking changes to OCP are announced at least five versions ahead of time, so developers have plenty of warning before something they rely on changes. The Nextcloud developer manual is the canonical reference for what counts as public.
OCP vs. OC
Within that structure, it’s worth understanding the difference between OCP and OC.
OCP is the public, documented, stable interface, and therefore always the safe choice.
OC is internal, often legacy, code. Some very old hooks still only exist in OC, but using it should be the rare exception, not the rule.
To give a concrete example: OC\Files\Storage\Local used to be a valid way to create local storage, but Nextcloud abstracted the storage layer to support things like S3 buckets and external drives, introducing IStorage in its place. The old OC class was eventually removed entirely, and any app still using it broke. As a rule of thumb, if something isn’t documented in the developer manual, assume it can change without warning, and avoid using it if there’s any alternative.
The request lifecycle
Every request into Nextcloud follows the same path from the moment it hits the server to the moment a response goes back out. Knowing that path, along with where security checks slot into it, makes it much easier to see where your own code belongs.
From request to response
Understanding how a request actually moves through Nextcloud helps explain where code belongs.
An HTTP request first hits index.php, which routes it through routes.php to determine which controller method should handle it. From there, the controller calls a service, which in turn talks to a storage or database layer, before the response travels back up the same chain.
This layered structure (controller, service, mapper, entity) will be familiar to anyone who’s worked with an MVC-style framework. Controllers translate the HTTP call into method calls, services do the actual work of processing and collecting data, and the split keeps the codebase testable and organized by responsibility.
Where security checks happen
Security checks (authentication, CSRF token verification, and rate limiting) all happen before a controller method ever runs. Brute-force protection is the one exception: it happens within the controller itself, which matters if you want to throttle anonymous requests to a specific endpoint, such as a public share link.
Controller attributes
Modern Nextcloud apps use PHP 8 attributes (the #[...] syntax) to declare how a controller method should be treated. For example, marking a route as a public page, exempting it from CSRF checks, or configuring brute-force throttling with a named action. Older apps may still use the previous approach (PHPDoc comments like @PublicPage), which still works but is considered legacy. New apps should use attributes.
Some attributes commonly appear together. For example, PublicPage and NoCSRFRequired often pair up for routes like a public profile page, where there’s no form being submitted and therefore nothing to protect with a CSRF token. The full reference of available attributes lives in the controllers section of the developer manual.
Dependency injection
Nextcloud relies heavily on dependency injection to keep code flexible and testable. Understanding how it works also explains why controllers receive a bare user ID instead of a full user object.
Interfaces instead of concrete classes
Nextcloud wires up functionality through dependency injection. Instead of a class instantiating its own dependencies, a container resolves them based on the interface being requested. So rather than injecting a concrete LocalStorage class, an app injects IStorage, and the container resolves it to whatever storage backend the admin has actually configured, be it a local disk, S3, or anything else. This also means developers can write their own storage backend (however unconventional) by implementing the IStorage interface, and any code that works against that interface will work with it automatically.
Dependency injection also makes testing dramatically easier, since dependencies like IAppConfig or IUserConfig can be swapped for fakes or mocks in unit tests, without needing a real database.
The nullable user ID
One Nextcloud-specific quirk worth knowing: Rather than injecting a full user object, controllers often receive a nullable string $userId, resolved automatically from the login session. It’s nullable because an anonymous visitor to a public page won’t have one. Even on pages that require authentication, it’s still good defensive practice to check that the user ID is actually set.
The reason Nextcloud injects just the ID rather than the full user object is efficiency. Resolving a full user object requires a database read, while the ID alone (which is unique and permanent, and once set, can never be changed) is often enough to check access to a data resource without hitting the database at all.
Data and events in Nextcloud
Nextcloud models data through entities backed by database migrations, keeps that data fast to query with well-placed indices, and reacts to changes across the system through events and background jobs. Getting this wrong can result in a slow query that only appears at scale, a migration that locks up an upgrade for hours, or an app that breaks the moment a dependency changes.
Entities and type casting
Entities are how Nextcloud represents a database row in code, and getting their type handling right is what keeps an app behaving consistently across every database it might end up running on.
Auto-generated getters and setters
An entity in Nextcloud is a representation of a database row. Rather than writing boilerplate getters and setters by hand, Nextcloud generates them automatically based on PHPDoc comments on the entity’s properties (for example, a userId property produces getUserId() and setUserId() for free).
Developers can still override a generated method if they need custom processing, such as converting a raw date field before returning it. But the recommended approach is to only do this when genuine processing is happening, since overriding a getter to reshape a database value can introduce subtle bugs.
Type casting across databases
Type casting is the other key concept here, covered in the manual’s database access section.
Nextcloud supports four different databases (MariaDB, MySQL, Oracle, and PostgreSQL) and they don’t agree on how they represent values like booleans or timestamps. Some databases store booleans as a tiny integer (0 or 1) and hand that back as a raw integer rather than a boolean. Without explicit type casting on the entity, a boolean field can come back as a string like "0", which behaves unexpectedly in conditional logic. Casting a field to datetime similarly guarantees you get a proper date object back instead of a raw string, regardless of which database is running underneath.
Migrations
Changing a database schema safely, across every database Nextcloud supports and every possible upgrade path a user might be on, means following a specific process. This process is easy to get right in principle and expensive to get wrong in practice.
The three-step process
Database schema changes in Nextcloud follow a three-step migration process, conceptually similar to migrations in frameworks like Laravel’s Eloquent:
A pre-schema change runs before the migration is applied, and is the place for anything that needs to happen to existing data before a table changes.
A schema change is where tables get created or altered. This is the actual DDL step.
A post-schema change runs after the schema is in place, and is typically used for lighter data migrations, like populating a new column from existing data.
The performance caveat
There’s an important caveat: Migrations run during install or upgrade, so any post-schema-change step that touches a large amount of data can seriously slow down that process. In a real use case, a migration for Nextcloud’s system address book once took six to seven hours on a lar