File-based inheritance
File-based inheritance is a powerful feature in Alokai that enables efficient code sharing and customization across multiple stores. It allows you to maintain a consistent functionality across multiple stores while customizing specific parts of the codebase for different brands or regions.
Imagine you're managing multiple online stores:
- A luxury fashion brand with a minimalist design
- A sports equipment store with dynamic, high-energy UI
While each brand needs its unique look and feel, you don't want to build everything from scratch every time. You also want to share common functionality like shopping carts and checkout flows between stores, while keeping brand-specific customizations separate.
This is where Alokai's file-based inheritance comes in. It lets you:
- Share common code between all your stores
- Customize specific components for each brand
- Create new stores quickly by inheriting existing functionality
- Maintain all stores efficiently from a single codebase
For example, your luxury brand and sports store could share the same checkout flow code, while having completely different product displays and navigation.
What You'll Learn
- Understanding file-based inheritance and its core concepts
- Different types of stores and when to use them
- How to create and manage stores using the CLI
- How store hierarchy and file overrides work
- How stores are composed and built
- Best practices and common pitfalls to avoid
Core Concepts
What is File-Based Inheritance?
File-based inheritance allows you to reuse common code across stores while enabling the customization of specific parts.
- Code Reuse
- Stores inherit all files from base applications by default
- Common functionality is maintained in one place
- Changes in base code automatically propagate to all stores
- Selective Customization
- Stores can override specific files when needed
- Only override what's different, inherit everything else
- Maintain the same file path structure for overrides
- Hierarchical Inheritance
- Stores can inherit from other stores
- Multiple levels of inheritance are supported
- More specific overrides take precedence over general ones
Project Structure
Every project starts with a single store called default
.
There is nothing special within the default
store, it is treated just like any other store. You can rename it to whatever you want.
Every store inherits from these base applications:
apps/storefront-unified-nextjs/ # Next.js Storefront
apps/storefront-unified-nuxt/ # Nuxt Storefront
apps/storefront-middleware/ # Middleware
apps/playwright/ # E2E tests
These base applications contain the default implementation of all features. When you create a new store, it automatically inherits all files from these base applications. You only need to override files when you want to customize specific functionality for your store.
Store Types
Alokai supports two types of stores to accommodate different business needs:
Deployable Stores
- Stores that are meant to be deployed and run
- Have their own build output in
.out/<store-id>
- Can override files from ancestor stores
- Can add store-specific files
- Example: An actual online store for a specific brand or region
Template Stores
- Stores that are never deployed
- Act as shared configuration/code providers for their descendant stores
- Used to group common overrides for a set of related stores
- No build output in
.out
directory - Example: A regional configuration store containing shared theme and regulations for all stores in that region
Creating New Stores
To create a new store, use the store add
command:
yarn store add
The CLI will guide you through configuring your store, including:
- Setting a unique store ID
- Choosing a parent store (or inheriting from base apps)
- Selecting the store type (template or deployable)
For detailed information about creating and managing stores, including configuration options and best practices, see the Managing the stores guide.
Managing Store Hierarchy
Store Structure
Simple hierarchy example with deployable stores only:
apps/
βββ storefront-unified-nextjs/ # Base shared code
βββ stores/
βββ brand-a/ # Deployable store
β βββ storefront-unified-nextjs/
βββ brand-b/ # Deployable store
βββ storefront-unified-nextjs/
Complex hierarchy example with both deployable and template stores:
apps/
βββ storefront-unified-nextjs/ # Base shared code
βββ stores/
βββ fashion-brand/ # Template store
β βββ storefront-unified-nextjs/ # Shared fashion brand customizations
β βββ stores/
β βββ us-store/ # Deployable store
β βββ eu-store/ # Deployable store
βββ sports-brand/ # Template store
βββ storefront-unified-nextjs/ # Shared sports brand customizations
βββ stores/
βββ us-store/ # Deployable store
βββ eu-store/ # Deployable store
Template vs Deployable Stores
Template stores (eg. fashion-brand
and sports-brand
) are used to share code between their child stores, while deployable stores (eg. us-store
and eu-store
) are the ones that get deployed and run. Template stores can define completely different UI components and styling, customer journey flows, product presentation, and regional adaptations that will be inherited by their deployable children.
When to Use Complex Hierarchies
Complex hierarchies make sense when you have multiple brands or store families that need different:
- Visual themes
- Features and functionality
- Business logic
- Regional customizations
In the example above, a single page is composed of:
navbar
component from the base appbody
from thefashion-brand
store, as it overrides the base app'sbody
footer
from theus-store
store, as it overrides the base app'sfooter
and thefashion-brand
store'sfooter
Keep it Simple
Only create inheritance levels when you need to share code between multiple stores. If you have just one brand or store family, keep everything in the base shared code (apps/storefront-unified-nextjs/
).
Moving Stores
For information about moving stores between parents, see the Managing the stores guide.
File Override System
Example: Let's say you want to customize the header component for your brand-a
store.
- Copy the original header from the base app:
# Original header
apps/storefront-unified-nextjs/components/header.tsx
# Copy it to your store's directory
apps/stores/brand-a/storefront-unified-nextjs/components/header.tsx
- Modify the copied file to match your brand's needs.
Now brand-a
will use its own version of header.tsx
, while inheriting all other files from the base application.
Always maintain the same file path structure when overriding files. For example, if you're overriding a component from apps/storefront-unified-nextjs/components/header.tsx
, place it in apps/stores/brand-a/storefront-unified-nextjs/components/header.tsx
in your store's directory.
File Resolution Process
When the CLI needs to compose the stores (during development or build), it follows a specific inheritance chain:
- First, it looks in the current store's directory
# For example, for us-store looking for header.tsx: apps/stores/fashion-brand/stores/us-store/storefront-unified-nextjs/components/header.tsx
- If not found, it checks ancestor stores in reverse order (closest parent first)
# First checks immediate parent (fashion-brand): apps/stores/fashion-brand/storefront-unified-nextjs/components/header.tsx
- Finally, it looks in the base applications
# Base app is the last resort: apps/storefront-unified-nextjs/components/header.tsx
The first matching file found is used. This allows stores to:
- Override specific files while inheriting others
- Share common customizations through parent stores
- Fall back to base functionality when no override exists
This resolution process applies to all file types: components, styles, configurations, etc. Understanding this helps you organize your overrides effectively.
Technical Implementation
TypeScript Configuration
The CLI automatically generates a tsconfig.json
for each store. It's worth noting that the following configuration ensures the store can import files from the base storefront, ancestor stores, and the store itself:
{
"compilerOptions": {
"paths": {
"@/*": ["./*", "../../../storefront-unified-nextjs/*"],
"@sf-modules/*": ["./sf-modules/*", "../../../storefront-unified-nextjs/sf-modules/*"]
},
"rootDirs": ["./", "../../../storefront-unified-nextjs"]
}
}
Do not modify tsconfig.json
The system automatically manages TypeScript configuration. Manual changes may break inheritance.
Change Detection
During the development process, the CLI:
- Automatically detects file changes in parent stores
- Propagates changes down the inheritance chain
- Respects overrides in child stores
You can read more about the change detection in the Using a local environment guide.
Store Composition and Build Output
How Stores are Composed
When building stores, the system:
- Collects all files from the base apps (
apps/storefront-unified-nextjs/
, etc.) - Applies overrides from parent stores in order
- Finally applies the store's own overrides
- Outputs the composed store to
.out/<store-id>/
For example, with this hierarchy:
apps/
βββ storefront-unified-nextjs/ # Base shared code
β βββ components/
β β βββ ProductCard.tsx # Original component
βββ stores/
βββ fashion-brand/
βββ storefront-unified-nextjs/
β βββ components/
β βββ ProductCard.tsx # Override 1
βββ stores/
βββ us-store/
βββ storefront-unified-nextjs/
βββ components/
βββ ProductCard.tsx # Override 2
The build process for us-store
would:
- Start with base files from
apps/storefront-unified-nextjs/
- Apply overrides from
fashion-brand
- Apply overrides from
us-store
- Output to
.out/us-store/
Build Output Structure
Each deployable store is built into its own directory:
.out/
βββ fashion-brand/
β βββ storefront-unified-nextjs/ # Composed Next.js app
β βββ storefront-middleware/ # Composed middleware
β βββ playwright/ # Composed tests
βββ us-store/
βββ storefront-unified-nextjs/ # Composed Next.js app
βββ storefront-middleware/ # Composed middleware
βββ playwright/ # Composed tests
Template Stores
Template stores are not built into the .out
directory because:
- They are used only for sharing code with descendant stores
- They don't have their own deployment or runtime
- They serve as configuration templates for their descendant stores
For example, if eu-region
is a template store with descendant stores fr-store
and de-store
, only fr-store
and de-store
will appear in the .out
directory. Any code or configuration in eu-region
will be inherited by its descendants but eu-region
itself won't be built.
File Selection
The system always selects the most specific override in the inheritance chain. If a file is overridden at multiple levels, the closest override to the current store takes precedence.
Special Files
Some files receive special treatment during composition:
package.json
: Cannot be overridden by the store. It is copied from the root apps with updated store-specific name.tsconfig.json
: Automatically generated for each store, based on thetsconfig.json
in the root apps.
Best Practices
- Minimize Overrides
- Only override files when necessary
- Consider creating shared components in parent stores
- Use configuration files for simple customizations
- Maintain Clear Structure
- Follow consistent directory organization
- Document store relationships
- Keep inheritance chains shallow when possible
- Testing
- Test changes across the entire inheritance chain
- Verify overrides don't break parent functionality
- Include tests for store-specific features
Common Pitfalls
- Incorrect File Placement
- Always place files within app-specific directories
- Maintain consistent paths across stores
- Breaking Changes
- Consider the impact on child stores when modifying shared code
- Test changes thoroughly across the inheritance chain
- Deep Inheritance
- Avoid deep inheritance chains (more than 3 levels)
- Consider flattening the structure if maintenance becomes difficult
- Special File Handling
package.json
:- Do not create this file within store directories
- The CLI automatically generates it in
.out
directory during build - Add all dependencies to the root app's
package.json
(e.g.,apps/storefront-unified-nextjs/package.json
)
tsconfig.json
:- Do not modify this file within store directories
- The CLI automatically generates and manages it
- Manual changes may break the inheritance system
eslint.config.mjs
:- Every app in every store has its own ESLint config file
- By default, it uses the shared ESLint config rules present in the
packages/eslint-config
- It is recommended to use the shared ESLint config to avoid duplication and ensure consistency across the project
prettier.config.mjs
:- Same as ESLint config
tailwind.config.ts
:- Same as ESLint config