Stack Innovations / Services / CMS / Strapi

Strapi, with the API actually engineered.

Most Strapi projects ship the default admin and a public REST endpoint. We treat Strapi as code — schema.json, custom controllers, lifecycle hooks, RBAC policies, dynamic zones tuned for editors, GraphQL on when it should be. Headless content with a head your team will fight to use.

01 — In numbers

Six years shipping
schemas on Node.

Each stat is a Strapi attribute. Hover any to see the JSON it'd ship as in schema.json — counters scrub with scroll, validations enforced, anime.js staggers the reveal.

projectsShipped
0+ Strapi projects shipped
"projectsShipped": {
  "type": "integer",
  "min": 32,
  "required": true
}
contentTypesModeled
0+ Content types modeled
"contentTypesModeled": {
  "type": "integer",
  "min": 1840,
  "required": true
}
customControllers
0+ Custom controllers & policies
"customControllers": {
  "type": "integer",
  "min": 210,
  "default": 0
}
defaultBuilds
0 Default-config Strapi builds
"defaultBuilds": {
  "type": "integer",
  "max": 0,
  "default": 0
}
02 — What we build

A full-stack Strapi practice.

Click the sidebar on the left — the active capability opens in the Content Manager, rendered as a real Strapi entry edit screen. Each pane is a working schema we've shipped.

Stack · Strapi v5 /admin/content-manager/collection-types/api::capability.studio prod S
collectionType api::capability.modeling PUBLISHED

Content-Type Builder, used like a tool, not a screen.

uid: api::capability.modeling updated: 09:42 today by: stack-eng
title · String

Content-Type Builder, used like a tool, not a screen.

summary · Text

We model collection types and single types like database tables — with foreign keys, validations, indexes — not by clicking through field pickers. Schemas live in src/api/*/content-types/*/schema.json, version-controlled, code-reviewed.

items · Component (repeatable)
  • Collection types & single types · modeled in JSON
  • Custom field types · React inputs in admin
  • Components · reusable across types
  • Validations · min, max, regex, custom
  • i18n · localized fields where it matters
collectionType api::capability.zones PUBLISHED

Components & Dynamic Zones, shipped to editors.

uid: api::capability.zones updated: yesterday by: stack-eng
summary · Text

Strapi's signature feature is the Dynamic Zone — an array of components, picked at edit time. We design the component library, lock the type signatures, and ship a renderer per surface. Editors compose pages, not posts.

items · Dynamic Zone
  • Components library · typed & reusable
  • Dynamic Zones · type: 'dynamiczone'
  • Per-component renderers (web, mobile, email)
  • Conditional fields · visible-when rules
collectionType api::capability.api PUBLISHED

REST & GraphQL, shaped to the screen.

uid: api::capability.api updated: 2d ago by: stack-eng
summary · Text

Strapi auto-generates REST and GraphQL from your schema. We pick the right one per surface, custom-controller the slow paths, layer field-level populate and fields filters, and put a CDN in front of it.

items · Component (repeatable)
  • Custom controllers · extended core
  • GraphQL · deep populates, resolvers
  • Field-level filters · fields=...&populate=...
  • Edge caching · webhooks · ISR
collectionType api::capability.rbac PUBLISHED

Roles & permissions, RBAC tuned for editors.

uid: api::capability.rbac updated: 4d ago by: stack-eng
summary · Text

Strapi ships RBAC. We tune it — field-level conditions, route policies, custom auth providers, scoped admin views. Editors see what they need, never what they don't.

items · Array of String
  • Custom roles · per-content-type
  • Field-level conditions
  • Route policies · policies/is-owner.js
  • SSO · SAML, OAuth, custom
collectionType api::capability.hooks PUBLISHED

Lifecycle hooks, where the business logic lives.

uid: api::capability.hooks updated: 1w ago by: stack-eng
summary · Text

beforeCreate, afterUpdate, beforeDelete. Strapi's lifecycle hooks are where slug generation, search indexing, webhook fan-out, audit logging, and cache invalidation belong — not in your frontend.

items · Array of String
  • Slug & cover-image autopopulate
  • Search index sync · Algolia / Meilisearch
  • Webhook fan-out · CDN purge, emails, Slack
  • Audit log · per-mutation events
collectionType api::capability.migrate PUBLISHED

Migrations & deploys, schema-aware, reversible.

uid: api::capability.migrate updated: 2w ago by: stack-eng
summary · Text

From WordPress, Contentful, Sanity, Drupal, or that homegrown CMS no one wants to touch. We model the schema first, write Knex migrations as reversible scripts, run them against a staging dataset, and cut over with redirects mapped.

items · Array of String
  • Knex · reversible migrations
  • Staging datasets · dry runs
  • Strapi → Strapi version bumps (v3 → v5)
  • 301s, sitemaps, SEO continuity
03 — Schema → API

Drag a field.
The API follows.

This is what makes Strapi itself: schemas are the API. Drag any field type from the palette onto the schema — the REST and GraphQL responses on the right rebuild in real time. anime.js draggable, popmotion inertia, mojs burst on save.

article.schema.json article.controller.ts routes.json
collectionType article uid: api::article.article
  • Aa title text required
  • / slug uid · from title
  • body richtext
  • cover media · image
  • author relation → person
drop a field here
REST GraphQL
live
GET /api/articles?populate=*

      
200 · 14ms 2.1 kB · gzip
04 — Why us

Stack Innovations vs the typical Strapi build.

Same platform, two article.controller.ts files. The diff tells the story.

typical/article.controller.ts — default Strapi
import { factories } from '@strapi/strapi'

export default factories.createCoreController(
  'api::article.article'
)

// no auth tuning
// no field filtering
// no rate-limit policy
// no audit log
// no error envelope
// shipped to prod — everywhere.
stack/article.controller.ts — Stack Innovations
import { factories } from '@strapi/strapi'
import { audit } from '../../../policies/audit'
import { rateLimit } from '../../../policies/rate'

export default factories.createCoreController(
  'api::article.article', ({ strapi }) => ({
    async find(ctx) {
      await rateLimit(ctx, { rpm: 60 })
      const { results, pagination } =
        await strapi.service('api::article.article')
          .findScoped(ctx.state.user, ctx.query)
      audit(ctx, 'article.read', results.length)
      return this.transformResponse(results, { pagination })
    },
    async create(ctx) {
      ctx.assertOwner()
      const out = await super.create(ctx)
      strapi.lifecycles.emit('article.created', out)
      return out
    },
  }))
typical · default factory, nothing customized Stack · scoped, audited, rate-limited, lifecycle-emitting
05 — Dynamic Zones

One page,
composed at edit time.

Dynamic Zones are Strapi's signature feature — an array of components, picked by an editor at runtime. Drag the blocks on the left to reorder, click the + to insert a component, watch the rendered preview rebuild on the right with popmotion inertia.

type · landing-page zone: blocks
  • 1 blocks.hero title · lede · ctas[]
  • 2 blocks.quote body · attribution
  • 3 blocks.gallery images[] · layout
  • 4 blocks.cta heading · button
+ add a component blocks.faq blocks.stats blocks.logos
stackinnovations.com/spring preview
06 — How it ships

Six weeks,
schema first.

  1. w 01

    Schema audit

    Audit the existing CMS, model collection types and components, lock the editor experience as a deliverable.

  2. w 02

    Strapi scaffold

    Strapi v5 scaffolded, schemas in JSON, environments configured (production / staging / dev), Postgres bootstrapped.

  3. w 02–04

    Custom controllers + zones

    Custom controllers, lifecycle hooks, components & dynamic zones, custom admin inputs, RBAC tuned.

  4. w 04

    Migration + SEO

    Knex migrations (reversible), staging dry-runs. Sitemaps, redirects, hreflang, schema markup.

  5. w 06

    Editor onboarding + launch

    Loom walkthroughs, role setup, dry-run publishes. DNS cutover. Slack handoff.

  6. Care plan

    Schema upkeep, Strapi version bumps, perf monitoring, monthly review. Features, not tickets.

Booking Q3 2026 · Avg engagement: 6 weeks · Reply within 24h