Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

319 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Flatbread 🥪

pipeline status Join the Flatbread slack NPM version

Turn files in Git into typed, related content for your TypeScript app. GraphQL and codegen are common ways to read that content, but they are not the only options. See Flatbread positioning.

Flatbread reads content from your repository and file system. Plugins control how it reads files and turns them into data.

Who it is for: Teams building TypeScript sites, internal tools, and starter projects that want versioned, reviewable content and links between entries without setting up a CMS database.

What Flatbread does not do:

  • It is not a hosted CMS, dashboard, or writing UI.
  • It is not a general-purpose GraphQL platform or database. Transactions, detailed access control, and many concurrent writers are outside its scope.
  • Run flatbread start --watch to update valid content and config changes while you work. Changes to Flatbread packages still need their own rebuild or restart. See the local development loop.

GraphQL: GraphQL is a common way to read Flatbread data. It does not define the product. For more detail, see Flatbread positioning.

Glossary: Definitions for collections, relations, IDs, validation, and the generated GraphQL types are in the glossary.

Local development: Learn what updates automatically and what needs a restart in the local development loop.

Export: The core API can create stable JSON snapshots. See JSON export.

Keeping your data: Your files, Git history, JSON/CSV exports, GraphQL introspection, and generated TypeScript remain available when you move away from Flatbread. See data ownership.

For contributing to this monorepo, use Node 20.19+ with pnpm 10.33.x. Runtime support for published packages is tracked by each package's own metadata.

Quickstart (posts, authors, and tags)

🚧 This project is experimental; the API may change before v1.0.

Start with the Next.js example in examples/nextjs. It reads shared Markdown from examples/content through its content/ symlink. The commands below use that layout.

1 · What you are modeling

  • Collections (Post, Author) map to folders of files; see the glossary.
  • Relations: posts declare authors: in frontmatter as a list of author ids; Flatbread resolves them through refs in config (same idea as joins, over files—not a remote database).
  • Tags: in the bundled example, each post exposes tags as a YAML string list in frontmatter. That becomes a [String] field on Post in the generated schema. That is facet-style metadata repeated per post—not the same machinery as refs to another collection. If you need normalized tag records shared across posts, model a Tag collection and wire refs yourself (advanced).

Illustrative frontmatter:

---
id: your-post-id
title: Example
authors:
  - author-id-one
tags:
  - typescript
  - content-graph
---

Markdown below the closing --- is the post body.

2 · Content layout (this monorepo)

From the repo root, the markdown that backs the relational story lives here:

examples/content/markdown/posts/     # Post collection (incl. example-post.md, …)
examples/content/markdown/authors/   # Author collection

The Next example points flatbread.config.js at content/markdown/... relative to examples/nextjs, where content is the symlink to ../content.

Backing files for posts, authors, and tags (this example):

What Where it lives Glossary terms
Posts examples/content/markdown/posts/*.md — one record per file Collection, Record
Authors examples/content/markdown/authors/*.md — one record per file Same; IDs in frontmatter wire relations
Tags The tags: YAML list in each post’s frontmatter (facet metadata on that Post). There is no markdown/tags/ directory here. Tag (facet) vs Tag collection

Traceability: same relation model (files, config, query interface)

The table below ties the Git-native model to the default GraphQL read layer without implying GraphQL is the product’s whole identity—GraphQL is one query interface; files and config remain the source of truth.

Layer You see… Glossary
Files authors: ids in a post file match id: in author files; tags: is a string list on the post Relation, ID, Tag (facet) vs Tag collection
flatbread.config.js content entries with collection: 'Post' | 'Author' and refs: { authors: 'Author' } Collection, Relation
Generated GraphQL schema + codegen TS allPosts { tags authors { id name } }refs resolve to Author objects; tags stays a scalar list on Post Generated schema and operation types (GraphQL), Cardinality

Illustrative query result (same relation model as examples/content/markdown/posts/example-post.md: authors 2a3e / 40s3, tags from frontmatter). Values are from that file and its resolved authors; the shape matches the GetPostsAuthorsAndTags operation in §3 after you include tags and authors in your .graphql document (see also queries/posts.graphql, which you can extend the same way):

{
  "allPosts": [
    {
      "id": "sdfsdf-23423-sdfsd-23444-dfghf",
      "title": "The Art of Measuring Cats in Fruit Units",
      "tags": ["cats", "measurements", "fruit-science", "important-research"],
      "authors": [
        { "id": "2a3e", "name": "Tony" },
        { "id": "40s3", "name": "Eva" }
      ]
    }
  ]
}

Add tags (and any other fields) to your .graphql documents and rerun codegen so operations and generated/graphql.ts stay aligned with the files—snippets in docs are illustrative until your checked-in queries match.

3 · Run it from the repo root

Prerequisites: Node 20.19+ and pnpm 10.33.x. See CONTRIBUTING.md.

pnpm install
pnpm build
cd examples/nextjs
pnpm exec flatbread codegen --verbose

That writes generated/graphql.ts: TypeScript types and typed document nodes for your .graphql operations (configure globs under codegen.documents in flatbread.config.js).

Add a .graphql file (see queries/posts.graphql in the example), then rerun pnpm exec flatbread codegen --verbose so the operation reflects tags, authors, etc. Illustrative operation you can paste into queries/:

query GetPostsAuthorsAndTags {
  allPosts(limit: 5) {
    id
    title
    tags
    authors {
      id
      name
    }
  }
  allAuthors {
    id
    name
  }
}

After codegen, your app imports types from ./generated/graphql. The result shape of that operation is typed (for example GetPostsAuthorsAndTagsQuery)—relations resolve to Author objects while tags stay a string array on Post, matching the file metadata—the same row as the illustrative JSON under Traceability.

The generated file also exposes a prototype TypeScript read API derived from the configured content model. In the Next.js example, examples/nextjs/lib/read.ts wires createFlatbreadReadApi() to the existing GraphQL fetcher and reads posts, authors, and tags with a generated default selection—no hand-written GraphQL document at the call site.

Choosing a read interface

Flatbread starts with related content files for TypeScript apps. Files define records, frontmatter fields, IDs, and refs; flatbread.config.js tells Flatbread how to group them into typed collections. GraphQL and the generated TypeScript API are two ways for your app to read the same data.

Use GraphQL operations when your app needs explicit query documents, custom selections, Apollo or other GraphQL clients, persisted operations, or direct access to the GraphQL endpoint. Add .graphql documents, include fields like tags and authors, and rerun codegen so operation types such as GetPostsAuthorsAndTagsQuery match the posts/authors/tags graph.

Use the prototype generated TypeScript read API when your app wants collection-shaped helpers for common reads from the configured Flatbread model, especially simple app reads such as posts, authors, tags, and resolved relations without writing GraphQL at each call site. The generated helpers currently execute through the GraphQL layer and still offer an experimental selection-string escape hatch, so both paths expose the same typed content graph backed by the same flat files while GraphQL remains the stable low-level interface.

Default filesystem + markdown wiring uses the bundled source-filesystem and transformer-markdown plugins (flatbread re-exports them).

4 · Minimal relational config (mental model)

The example’s production config loads extra collections for tests; the core onboarding shape is:

import { defineConfig, transformerMarkdown, sourceFilesystem } from 'flatbread';

export default defineConfig({
  source: sourceFilesystem(),
  transformer: transformerMarkdown({
    markdown: { gfm: true, externalLinks: true },
  }),
  content: [
    {
      path: 'content/markdown/posts',
      collection: 'Post',
      refs: { authors: 'Author' },
    },
    {
      path: 'content/markdown/authors',
      collection: 'Author',
      refs: { friend: 'Author' },
    },
  ],
});

5 · Reading the graph: GraphQL (after the model exists)

Flatbread builds a content graph from files. In the default toolchain, GraphQL is one read interface: schema + resolver shape over that graph—not “Flatbread is a GraphQL CMS.”

Wire your framework so the CLI wraps dev/build (flatbread start passes through your command after --). There is no flatbread dev subcommand.

// package.json scripts (adapt the part after `--` to your framework)
{
  "scripts": {
    "dev": "flatbread start --watch -- next dev --turbopack",
    "build": "flatbread start -- next build"
  }
}

In the Next.js example, pnpm dev starts Next with local HTTPS and starts Flatbread in watch mode. The GraphQL endpoint is http://localhost:5057/graphql and the Next app is on 3000. pnpm start runs production Next without Flatbread.

pnpm dev

When the server starts, Flatbread prints the graphql URL. Open it to use Apollo Studio with the generated schema. You can then save queries in .graphql files and run flatbread codegen again.

With --watch, valid content and config changes update the running GraphQL server. See the local development loop for the cases that still need a rebuild or restart.

Install Flatbread in your own repo

Outside this monorepo:

pnpm add flatbread@latest

Scaffold flatbread.config.js:

pnpm exec flatbread init

Point content entries at your posts/ and authors/ folders, reuse the relational ideas above, and add codegen in config when you want generated/graphql.ts. Browse packages for plugins and resolver helpers.

More detail on the bundled example is in the Next.js example README.

Query arguments (GraphQL read interface)

When GraphQL is your read interface, list fields use the following arguments in order of application.

filter

Each collection in the GraphQL schema can be passed a filter argument to constrain your results, sifting for only what you want. Any leaf field should be able to be used in a filter.

The syntax for filter is based on a subset of MongoDB's query syntax.

filter syntax

A filter is composed of a nested object with a shape that matches the path to the value you want to compare on every entry in the given collection. The deepest nested level that does not have a JSON object as its value will be used to build the comparison where the key is the comparison operation and value is the value to compare every entry against.

Example

filter = { postMeta: { rating: { gt: 80 } } };

entries = [
  { id: 1, title: 'My pretzel collection', postMeta: { rating: 97 } },
  { id: 2, title: 'Debugging the simulation', postMeta: { rating: 20 } },
  {
    id: 3,
    title: 'Liquid Proust is a great tea vendor btw',
    postMeta: { rating: 99 },
  },
  { id: 4, title: 'Sitting in a chair', postMeta: { rating: 74 } },
];

The above filter would return entries with a rating greater than 80:

result = [
  { id: 1, title: 'My pretzel collection', postMeta: { rating: 97 } },
  {
    id: 3,
    title: 'Liquid Proust is a great tea vendor btw',
    postMeta: { rating: 99 },
  },
];

Supported filter operations

  • eq - equal
    • This is like filterValue === resultValue in JavaScript
  • ne - not equal
    • This is like filterValue !== resultValue in JavaScript
  • in
    • This is like filterValue.includes(resultValue) in JavaScript
    • Can only be passed an array of values which pass strict comparison
  • nin
    • This is like !filterValue.includes(resultValue) in JavaScript
    • Can only be passed an array of values which pass strict comparison
  • includes
    • This is like resultValue.includes(filterValue) in JavaScript
    • Can only be passed a single value which passes strict comparison
  • excludes
    • This is like !resultValue.includes(filterValue) in JavaScript
    • Can only be passed a single value which passes strict comparison
  • lt, lte, gt, gte
    • This is like <, <=, >, >= respectively
    • Can only be used with numbers, strings, and booleans
  • exists
    • This is like filterValue ? resultValue != undefined : resultValue == undefined
    • Accepts true or false as a value to compare against (filterValue)
    • For checking against a property that could be both null or undefined
  • strictlyExists
    • This is like filterValue ? resultValue !== undefined : resultValue === undefined
    • Accepts true or false as a value to compare against (filterValue)
    • Checking against a property for undefined
  • regex
    • This is like new RegExp(filterValue).test(resultValue) in JavaScript
  • wildcard
    • This is an abstraction on top of regex for loose string matching
    • Case insensitive
    • Uses matcher and matcher's API

Caveats:

Combining multiple filters

You can union multiple filters together by adding peer objects within your filter object to point to multiple paths.

Example

Using the entries from the previous example, let's combine multiple filters.

query FilteredPosts {
  allPosts(
    filter: { title: { wildcard: "*tion" }, postMeta: { rating: { gt: 80 } } }
  ) {
    title
  }
}

Results in:

result = [{ title: 'My pretzel collection' }];

sortBy

Sorts by the given field. Accepts a root-level field name. Defaults to not sortin' at all.

order

The direction of sorting. Accepts ASC or DESC. Defaults to ASC.

skip

Skips the specified number of entries. Accepts an integer.

limit

Limits the number of returned entries to the specified amount. Accepts an integer.

Query from your app

Follow Quickstart (posts, authors, and tags) to model related content, run codegen, and get typed results. For scripts and framework setup, use the Next.js example.

Field overrides

Field overrides allow you to define custom GraphQL types or resolvers on top of fields in your content. For example, you could optimize images, encapsulate an endpoint, and more!

Example

const config = {
  content: {
    overrides: [
      {
        // The source field name.
        field: 'name',
        // The GraphQL type to expose.
        type: 'String',
        // Capitalize the value before returning it.
        resolve: (name) => capitalize(name),
      },
    ],
  },
};

Supported syntax for field

  • basic nested objects

    nested.object

  • a basic array (will map array values)

    an.array[]

  • a nested object inside an array (will also map array)

    an.array[]with.object

for more information in Overrides, they adhere to the GraphQLFieldConfig outlined here https://graphql-compose.github.io/docs/basics/what-is-resolver.html

Advanced Config

fieldNameTransform

Accepts a function which takes in field names and transforms them for the GraphQL schema generation -- this is used internally to remove spaces but can be used for other global transforms as well

{
  ...
  // replace all spaces in field names with an underscore
  fieldNameTransform: (fieldName) => fieldName.replace(/\s/g, '_')
  ...
}

☀️ Contributing

See CONTRIBUTING.md for release steps, including version bumps and publishing.

About

Consume relational, flat-file data using GraphQL in any static framework 🫓

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages