Skip to main content

Overview

The Custom pattern starts your application as a blank canvas. Instead of a preset tier, you choose a network configuration, then add each compute, database, and storage resource yourself with fjall add.
fjall create app --type custom
Use Custom when no preset tier fits, when you are learning how Fjall composes resources, or when you need full control over the architecture.

Prerequisites

RequirementDetail
Fjall CLIInstalled and authenticated (fjall login)
Node.jsVersion 22 or later
AWS accountConnected via fjall connect

Interactive Flow

A Custom application is created in three short steps, then you add resources afterwards.
? Choose your starting point? Custom
? Name your application? api
? Network configuration?
  > Standard      Production ready · 2 AZs, 1 NAT Gateway, KMS
    Lightweight   Cost effective · 2 AZs, shared NAT
    Resilient     High availability · 3 AZs, 3 NAT Gateways, CMK Keys
    Tinkerer      Free tier · 2 AZs, no NAT Gateway
    No network    Standalone resources · S3, CDN, Lambda, EventBridge
The review then confirms a blank canvas:
Resources: Blank canvas (add resources after creation)
A Custom application does not prompt for compute, database, or storage during creation. Pick a network preset (or No network for standalone resources), confirm, then add resources with fjall add.

Adding Resources

Add each resource to the generated infrastructure.ts with fjall add <type> --name <PascalCaseName>. The --name is required and PascalCase.
# Add an ECS service
fjall add compute --name Api --type ecs

# Add a database (variant is the --type property)
fjall add database --name UserDb --type Aurora

# Add an S3 bucket
fjall add storage --name Uploads
Valid resource types: database, storage, compute, messaging, cdn, network, pattern, vpc-peer, vpc-peer-accepter, cross-plan-connection. Database variant is a property, not part of the type. Pass --type <variant> where the variant is one of Aurora, Instance, GlobalAurora, ClickHouse, or DynamoDB.

Building Custom Architectures

Each fjall add writes a factory call into infrastructure.ts. The examples below show the generated code for common architectures so you can see what each combination produces.

Microservices Architecture

import {
  App,
  ComputeFactory,
  DatabaseFactory,
} from "@fjall/components-infrastructure";
import { Code, FunctionUrlAuthType } from "aws-cdk-lib/aws-lambda";

const app = App.getApp("microservices");

// API gateway
const apiGateway = app.addCompute(
  ComputeFactory.build("ApiGateway", {
    type: "lambda",
    deployment: "code",
    code: Code.fromAsset("lambda/api-gateway"),
    handler: "index.handler",
    functionUrl: { authType: FunctionUrlAuthType.NONE },
  }),
);

// User service
const userDb = app.addDatabase(
  DatabaseFactory.build("UserDb", {
    type: "Aurora",
    databaseName: "Users",
  }),
);

const userService = app.addCompute(
  ComputeFactory.build("UserService", {
    type: "ecs",
    services: [
      {
        name: "user",
        capacityProvider: "FARGATE",
        containers: [{ port: 3001 }],
      },
    ],
  }),
);

// Order service
const orderDb = app.addDatabase(
  DatabaseFactory.build("OrderDb", {
    type: "Instance",
    databaseName: "Orders",
  }),
);

const orderService = app.addCompute(
  ComputeFactory.build("OrderService", {
    type: "ecs",
    services: [
      {
        name: "order",
        capacityProvider: "FARGATE",
        containers: [{ port: 3002 }],
      },
    ],
  }),
);

Event-Driven Architecture

// Event processor
const eventProcessor = app.addCompute(
  ComputeFactory.build("EventProcessor", {
    type: "lambda",
    deployment: "code",
    code: Code.fromAsset("lambda/processor"),
    handler: "index.handler",
    memorySize: 1024,
    timeout: 300,
  }),
);

// Event store
const eventStore = app.addDatabase(
  DatabaseFactory.build("EventStore", {
    type: "Aurora",
    databaseName: "Events",
  }),
);

// API layer
const api = app.addCompute(
  ComputeFactory.build("Api", {
    type: "ecs",
    services: [
      {
        name: "api",
        capacityProvider: "FARGATE",
        containers: [{ port: 3000 }],
      },
    ],
  }),
);

Batch Processing

// Job queue database
const jobDb = app.addDatabase(
  DatabaseFactory.build("JobDb", {
    type: "Instance",
    databaseName: "JobQueue",
    instanceType: "t4g.micro", // Free tier eligible
  }),
);

// Batch processor
const batchProcessor = app.addCompute(
  ComputeFactory.build("BatchProcessor", {
    type: "ecs",
    services: [
      {
        name: "batch",
        capacityProvider: "FARGATE_SPOT", // Cost-optimised
        cpu: 2048,
        memoryLimitMiB: 4096,
        containers: [{ port: 8080 }],
      },
    ],
  }),
);

// Scheduler
const scheduler = app.addCompute(
  ComputeFactory.build("Scheduler", {
    type: "lambda",
    deployment: "code",
    code: Code.fromAsset("lambda/scheduler"),
    handler: "index.handler",
  }),
);

Hybrid Architecture

// Legacy system connector
const legacyConnector = app.addCompute(
  ComputeFactory.build("LegacyConnector", {
    type: "ec2",
    instanceType: "t4g.large",
    ssh: {},
  }),
);

// Modern API
const modernApi = app.addCompute(
  ComputeFactory.build("ModernApi", {
    type: "lambda",
    deployment: "code",
    code: Code.fromAsset("lambda/api"),
    handler: "index.handler",
    environment: {
      LEGACY_ENDPOINT: "10.0.1.100",
    },
  }),
);

// Shared database
const sharedDb = app.addDatabase(
  DatabaseFactory.build("SharedDb", {
    type: "Instance",
    databaseName: "Shared",
  }),
);

Mix and Match Resources

The Custom pattern supports any combination of compute and database types.
// Mix compute types
const lambda = app.addCompute(
  ComputeFactory.build("Lambda", {
    type: "lambda",
    deployment: "code",
    handler: "index.handler",
  }),
);
const ecs = app.addCompute(
  ComputeFactory.build("Ecs", {
    type: "ecs",
    services: [
      {
        name: "app",
        capacityProvider: "FARGATE",
        containers: [{ port: 3000 }],
      },
    ],
  }),
);
const ec2 = app.addCompute(
  ComputeFactory.build("Ec2", { type: "ec2", instanceType: "t4g.micro" }),
);

// Mix database types
const aurora = app.addDatabase(
  DatabaseFactory.build("Aurora", { type: "Aurora", databaseName: "app" }),
);
const instance = app.addDatabase(
  DatabaseFactory.build("Instance", { type: "Instance", databaseName: "app" }),
);
const freeTier = app.addDatabase(
  DatabaseFactory.build("FreeTierDb", {
    type: "Instance",
    databaseName: "app",
    instanceType: "t4g.micro",
  }),
);

When to Use

Choose Custom for:
  • Unique architectural requirements
  • Proof of concepts
  • Learning how Fjall composes resources
  • Maximum control over every resource
Choose a preset tier for:
  • Standard architectures (Standard or Resilient)
  • Quick deployment with opinionated defaults
  • Free experimentation (Tinkerer)

Tips

  • Plan your architecture before adding resources.
  • Start with one compute resource and add more incrementally.
  • Use PascalCase names that describe each resource (Api, UserDb, Uploads).
  • Match resource types to cost targets (t4g.micro instances and FARGATE_SPOT for low-cost workloads).

Common Custom Architectures

ArchitectureResources to add
API + workercompute Api (ECS Fargate), compute Worker (ECS Fargate Spot), database SharedDb (Aurora)
Static site + APIcompute Web (Lambda with function URL), compute Api (ECS Fargate), database AppDb (Instance)
Data pipelinecompute Ingest (Lambda), compute Process (ECS Fargate Spot), database Store (Aurora)

Next Steps

Compute Factory

ECS, Lambda, and EC2 options for compute resources.

Database Factory

Aurora, Instance, DynamoDB, and ClickHouse variants.

Storage Factory

Add S3 buckets to your application.

Deploy Application

Ship your custom architecture to AWS.