A Introduction#

FRAGMENT is a toolkit for building products that move and track money. It includes an API and dashboard for designing, implementing, and operating your Ledger.

Don't have a FRAGMENT workspace? Get access.

B Design your Ledger#

A Ledger uses a Schema to define functionality for a specific product and use case. A Schema may be shared across multiple Ledgers. Updating a Schema will trigger migrations to update each Ledger. Use the Ledger designer in the Dashboard to model and store your Schema.

Ledgers track money using:

  • Ledger Accounts, balances that represent the financial state of a business
  • Ledger Entries, financial events that update Ledger Accounts

a. Ledger Accounts

#

A Ledger Account has a balance. Changes to a Ledger Account's balance are called Ledger Lines.

There are four types of Ledger Accounts, split into two layers:

State

Assets: what you own
Liabilities: what you owe

Change

Income: what you've earned
Expense: what you've spent

State Ledger Accounts track your product's financial relationships with your bank, payment systems and users. Balance Sheets are a report of State Ledger Account balances.

Change Ledger Accounts track when and how your product makes a profit or loss. They produce Income Statements.

Within a Schema, the chartOfAccounts key contains a nested tree of Ledger Accounts, up to a maximum depth of 10:

Ledger Accounts that share a parent require a unique key, but the same key can be used in different parts of the tree.

For some Ledger Accounts, you must set additional properties:

  • linkedAccount enables reconciliation with an external system
  • template allows multiple instances to be created on demand
  • currencyMode configures the account's currency mode, single or multi
  • consistencyConfig configures the whether the Ledger Account's balances and Ledger Lines are strongly or eventually consistent

b. Ledger Entries

#

A Ledger Entry is a single update to a Ledger. Define a Ledger Entry type in your Schema for every financial event in your product and bank.

A Ledger Entry must be balanced, which means it follows the Accounting Equation:


Assets - Liabilities = Income - Expense

How you balance a Ledger Entry depends upon its net effect to the Ledger's balances.

When the net change to the State Ledger Accounts is zero, the financial event being recorded did not change the net worth of the business. In this example, an increase to an asset account is balanced by an increase in a liability account:

When the net change to the State Ledger Accounts is non-zero, the financial event being recorded made a profit or loss. In this example, a difference in asset and liability accounts is balanced by an increase in an income account.

C Deploy your Ledger#

Once you've designed your Ledger, you can deploy it using the dashboard, the API, or by embedding our CLI in your CI.

a. Dashboard

#

You can edit and store your Schema and create Ledgers directly from the FRAGMENT Dashboard. This is useful during development, but is not recommended for production workflows.

b. API

#

You can call the API to store your Schema and create Ledgers. This is useful if you want to automate your Ledger deployment or have multiple environments which you want to keep in-sync. If you are creating many Schemas and Ledgers, you can also call the storeSchema and createLedger APIs directly from your product.

Call storeSchema to store a Schema. Depending on your use case, you may share one Schema for all your users, or create a Schema per user.

storeSchema query
mutation QuickstartStoreSchema($schema: SchemaInput!) {
  storeSchema(schema: $schema) {
    ... on StoreSchemaResult {
      schema {
        key
        name
        version {
          version
          created
          json
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}
storeSchema variables
{
  "schema": {
    "key": "quickstart-schema",
    "name": "Quickstart Schema",
    "chartOfAccounts": {
      "defaultCurrency": {
        "code": "USD"
      },
      "defaultCurrencyMode": "single",
      "accounts": [
        {
          "key": "assets",
          "type": "asset",
          "children": [
            {
              "key": "banks",
              "children": [
                {
                  "key": "user-cash"
                }
              ]
            }
          ]
        },
        {
          "key": "liabilities",
          "type": "liability",
          "children": [
            {
              "key": "users",
              "template": true,
              "consistencyConfig": {
                "ownBalanceUpdates": "strong"
              },
              "children": [
                {
                  "key": "available"
                },
                {
                  "key": "pending"
                }
              ]
            }
          ]
        },
        {
          "key": "income",
          "type": "income"
        },
        {
          "key": "expense",
          "type": "expense",
          "children": []
        }
      ]
    },
    "ledgerEntries": {
      "types": [
        {
          "type": "user_funds_account",
          "description": "Funding {{user_id}} for {{funding_amount}}.",
          "lines": [
            {
              "account": { "path": "assets/banks/user-cash" },
              "key": "funds_arrive_in_bank",
              "amount": "{{funding_amount}}"
            },
            {
              "account": { "path": "liabilities/users:{{user_id}}/available" },
              "key": "increase_user_balance",
              "amount": "{{funding_amount}}"
            }
          ]
        },
        {
          "type": "p2p_transfer",
          "description": "P2P of {{transfer_amount}} from {{from_user_id}} to {{to_user_id}}.",
          "lines": [
            {
              "account": {
                "path": "liabilities/users:{{from_user_id}}/available"
              },
              "key": "decrease_from_user",
              "amount": "-{{transfer_amount}}"
            },
            {
              "account": { "path": "liabilities/users:{{to_user_id}}/available" },
              "key": "increase_to_user",
              "amount": "{{transfer_amount}}"
            }
          ],
          "conditions": [
            {
              "account": {
                "path": "liabilities/users:{{from_user_id}}/available"
              },
              "postcondition": {
                "ownBalance": {
                  "gte": "0"
                }
              }
            }
          ]
        }
      ]
    }
  }
}
 

Create a Ledger using the createLedger mutation.

createLedger
mutation QuickstartCreateLedger(
  $ik: SafeString!
  $ledger: CreateLedgerInput!
  $schema: SchemaMatchInput
) {
  createLedger(
    ik: $ik,
    ledger: $ledger,
    schema:$schema
  ) {
    ... on CreateLedgerResult {
      ledger {
        ik
        name
        created
        schema {
          key
        }
      }
    }
    ... on Error {
      code
      message
    }
  }
}

The schema.key field is set to the key from the storeSchema API call.

createLedger variables
{
  "ik": "quickstart-ledger",
  "ledger": {
    "name": "Quickstart Ledger"
  },
  "schema": {
    "key": "quickstart-schema"
  }
}

c. CLI

#

The FRAGMENT CLI can be installed in your CI and used to store your Schema.

Here's an example of how you can use the CLI in a Github Action workflow:

Fragment CLI in Github Actions
- name: Set up Homebrew
  id: set-up-homebrew
  uses: Homebrew/actions/setup-homebrew@master
- name: Install Fragment CLI
  run: |
    brew tap fragment-dev/tap
    brew install fragment-dev/tap/fragment-cli
    echo "Fragment CLI installed"
- name: Authenticate with Fragment
  run: |
    fragment login \
      --client-id ${{ vars.FRAGMENT_CLIENT_ID }} \
      --client-secret ${{ vars.FRAGMENT_CLIENT_SECRET }} \
      --api-url ${{ vars.FRAGMENT_API_URL }} \
      --oauth-url ${{ vars.FRAGMENT_OAUTH_URL }} \
      --oauth-scope ${{ vars.FRAGMENT_OAUTH_SCOPE }}
- name: Store Schema
  run: |
    fragment store-schema --path my-schema.jsonc
 
D Integrate the API#

This section covers how to build a reliable and fault tolerant integration with FRAGMENT.

To integrate the API, you need to:

  1. Select the AWS region for your workspace.
  2. Generate an SDK based off your GraphQL queries and mutations.
  3. Authenticate your API client.
  4. Call the API with an idempotency key.
  5. Handle errors from the API.
  6. Decide whether to call the API synchronously or asynchronously.

a. Select region

#

FRAGMENT has a GraphQL API to read and write data from your Ledger. The API is available in the following AWS regions:

  • us-east-1
  • us-east-2
  • us-west-2
  • eu-west-1

You can find the region for your workspace in the API URL in the settings tab of the dashboard. If you need to create a new workspace, you can do so through the top-left dropdown in the dashboard.

If you don't see your desired AWS region, contact us.

b. Generate an SDK

#

To ensure you always make valid queries, generate a typed SDK for your programming language. Do so using an SDK codegen tool and the current version of the FRAGMENT GraphQL schema.

FRAGMENT hosts the latest GraphQL schema at:

https://api.fragment.dev/schema.graphql

Use this URL as the SDK codegen input. All changes to the GraphQL API are backwards-compatible. You only need to generate a new version of the SDK to use new features.

Typescript#

graphql-codegen is a tool for generating Typescript SDKs:

  1. Add necessary dependencies to your package.json:
    • @graphql-codegen/typescript-graphql-request
    • @graphql-codegen/cli
    • graphql-request
    • graphql-tag
yarn add @graphql-codegen/typescript-graphql-request \
  @graphql-codegen/cli \
  graphql-request \
  graphql-tag
  1. Configure a codegen.yml file. A starter configuration is provided below.
  2. Define the GraphQL queries that your app uses in the queries folder.
  3. Generate the SDK by calling yarn graphql-codegen.

An example codegen.yml:

codegen.yml

schema: "https://api.fragment.dev/schema.graphql"
generates:
  fragment-sdk.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-graphql-request
      - add:
          content: "/* This file was auto-generated. Don't edit it directly. See 'codegen.yml' */"
    documents: './queries/**/*.ts'
    config:
      scalars:
        Int96: string

As an example, to generate a typed call to addLedgerEntry, define your query in ./queries/addLedgerEntry.ts as:

addLedgerEntry.ts

import gql from 'graphql-tag';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const QUERY = gql`
  mutation addLedgerEntry(
    $entry: LedgerEntryInput!,
    $ik: SafeString!
  ) {
    addLedgerEntry(ik: $ik, entry: $entry) {
      __typename
      ... on AddLedgerEntryResult {
        entry {
          id
          posted
          created
          date
          description
        }
        lines {
          id
          date
          posted
          created
          amount
          type
          ledgerId
          accountId
          ledgerEntryId
        }
        isIkReplay
      }
      ... on Error {
        code
        message
      }
    }
  }
`;

c. Authenticate

#

FRAGMENT uses OAuth2's client credentials flow to authenticate API clients. The call requesting an access token follows the OAuth2 spec. You can use any OAuth2 library that supports the client credentials grant to retrieve the token or make an HTTP request. The flow is:

  1. Create an API client in the FRAGMENT dashboard. All API Clients have full access to your workspace.
  2. Note the auth endpoint URL and OAuth scope for your API Client. This is specific to the AWS region of your workspace.
  3. Get a fresh access token from the token endpoint. The access token expires in 1 hour. We recommend retrieving a new token for each set of calls you make. You can generate and use multiple tokens at the same time.
  4. Use that token in an Authorization header with the value Bearer {{access_token}} when calling the GraphQL API.

Your token request payload should be in x-www-form-urlencoded format, with the keys grant_type, scope and client_id.

Token request body
grant_type=client_credentials&scope={{scope}}&client_id={{client_id}}

Your token request headers should contain the following, where client_credentials is the Base64-encoded version of: {{client_id}}:{client_secret}}.

Token request headers
{
    "Content-Type": "application/x-www-form-urlencoded",
    "Authorization": "Basic {{client_credentials}}",
    "Accept": "*/*"
}

The response is a JSON object containing your access token.

Token response body
{
    "access_token": "<access token>",
    "expires_in": 3600,
    "token_type": "Bearer"
}

Putting it all together:

Javascript