Child database access

A multi-tenant database architecture allows a single database to serve multiple tenants or customers. Typically, database systems that provide this feature have multiple databases each with their own access control. Fauna extends this multitenancy capability by allowing you to create parent-child hierarchies in a single Fauna database.

Using the multitenancy feature, a single database in your organization can serve multiple teams through child databases. As a tenant, a child database has schemas independent from the parent. A team can create collections, documents, indexes, and even other child databases in their child without impacting the parent or other child databases.

By creating access keys on the child databases, teams have dedicated access keys for their own child database. When connected to a child database, it is not possible to access the parent database, other child databases, or even to discover that these exist. A parent database admin key holder can revoke a child access key at any time. A parent database server key holder cannot manage the child databases.

Create child databases

This procedure creates two child databases in an existing CoffeeStore parent database.

  1. Log in to the Dashboard.

  2. Click the CoffeeStore database to open it and reveal the Fauna Shell.

  3. Ensure you have an existing coffee-admin and coffee-server key by listing the keys.

    Key.all()

    The coffee-admin key manages the CoffeeStore parent and any child databases it might have. If the coffee-admin key does not exist, create it but remember to save the secret in a safe place. The Get started explains how to setup for the example.

  4. Create two child databases, one called production and one called internal.

    [{name: "production"}, {name: "internal"}].map(Database.create)
    [
      {
        name: "production",
        coll: Database,
        ts: Time("2023-07-01T22:38:03.270Z"),
        global_id: "ywxwmh6o4ybng"
      },
      {
        name: "internal",
        coll: Database,
        ts: Time("2023-07-01T22:38:03.270Z"),
        global_id: "ywxwmh6ohybng"
      }
    ]
  5. Click Resources in the left navigation panel an expand the Databases list. You should see two nodes, one for each child:

    You can also list the databases using the Database.all() query.

Create admin keys for child databases

The admin keys on a parent database can manage the child databases. To allow users to manage a child database, create a key with the admin role on that child database.

  1. Make sure your Shell is open on the CoffeeStore database, this is the parent database.

  2. Create admin secrets for each child database.

    ["production", "internal"].map(db => Key.create({role: "admin", database: db}))
    [
      {
        id: "369091607961535011",
        coll: Key,
        ts: Time("2023-07-01T22:53:57.030Z"),
        role: "admin",
        database: "production",
        secret: "fnAFH0bcpiACI-pzd2WhCxKlFCZqHf7ZJWZ9YQ54"
      },
      {
        id: "369091607961536035",
        coll: Key,
        ts: Time("2023-07-01T22:53:57.030Z"),
        secret: "fnAFH0bcpiAGI8Wu5OAA2cKMv6vkxc5MFwvGekNp",
        role: "admin",
        database: "internal"
      }
    ]

    At creation is the only time Fauna displays the secret field. The value in this field is equivalent to a password. Fauna cannot recover a secret that is discarded or lost. Copy and save the secret to a password manager or other safe location. Delete and replace keys or tokens for which you have lost the secret. If you no longer need a key or token, you should delete it.

  3. Take a moment to save the secrets for these new keys. You need them in the next procedure.

  4. To find all the keys that are for a child database, list keys that have a database field.

    Key.all().where(.database != null)

    This is an efficient query for long lists of keys. Only child database keys have a database field.

  5. Navigate to the CoffeeStore/production database by selecting it from the Database tree.

  6. Query for a list of keys in the production database

    Key.all()
    {
      data: []
    }

    The list in the child database is empty. The keys you created in the parent database are on the parent, they are not visible from the child.

Add internal child databases

The internal team wants two child databases, named personnel and bulletinBoard. The team also has an application for each of its child databases.

Keys with the server role are equivalent to the admin role with some exceptions. Child databases, keys, tokens, and their associated documents cannot be directly managed with server role privileges. A server key can be used to connect to their associated database. The internal team applications can use server keys to connect to the personnel and bulletinBoard databases and manage non-native collections.

Create two child databases and a server key for each child database.

  1. Navigate to the CoffeeStore/internal database by selecting it from the Database tree.

  2. Set the run menu to Secret.

  3. Enter the CoffeeStore/internal admin secret you created in the last procedure.

  4. Create the two child databases:

    [{name: "personnel"}, {name: "bulletinBoard"}].map(Database.create)
    [{name: "personnel"}, {name: "bulletinBoard"}].map(Database.create)
    
    [
      {
        name: "personnel",
        coll: Database,
        ts: Time("2023-07-02T18:16:35.780Z"),
        global_id: "ywxa1ahhaydng"
      },
      {
        name: "bulletinBoard",
        coll: Database,
        ts: Time("2023-07-02T18:16:35.780Z"),
        global_id: "ywxa1ahh4ybng"
      }
    ]
  5. Create server keys for use in each of these new child databases.

    ["personnel", "bulletinBoard"].map(db => Key.create({role: "server", database: db}))
    [
      {
        id: "369164880539813411",
        coll: Key,
        ts: Time("2023-07-02T18:18:35.130Z"),
        role: "server",
        database: "personnel",
        secret: "fnAFH4mAwCAGI_aCpNCqX5WWf3f-Y5fWXI9XSizX"
      },
      {
        id: "369164880539812387",
        coll: Key,
        ts: Time("2023-07-02T18:18:35.130Z"),
        secret: "fnAFH4mAwCACI8zdIiQRKDGeNStRFLDttYI56d3w",
        database: "bulletinBoard",
        role: "server"
      }
    ]
  6. Save copies of the secrets for use later in this cookbook.

    At creation is the only time Fauna displays the secret field. The value in this field is equivalent to a password. Fauna cannot recover a secret that is discarded or lost. Copy and save the secret to a password manager or other safe location. Delete and replace keys or tokens for which you have lost the secret. If you no longer need a key or token, you should delete it.

Access a child database from an application

If you have completed the previous section of the cookbook, Keys and built-in roles, you should already have the coffeestore.mjs file application available in your environment.

  1. Edit the coffeestore.mjs file and replace the try-catch block with this one:

    try {
      // build queries using the fql function
      const collection_query = fql`Collection.create({ name: "Post" })`;
    
      // Execute the query, return more informative decorated format
      // The decorated format is not suitable for complex objects in production systems
      const collection_result = await client.query(collection_query, {format:
      "decorated"});
    
      console.log(collection_result);
      console.log(collection_result);
    
      // define some data in your app
      const post = { headline: "Lost Doggo" };
    
      // query using your application local variables
      const document_query = fql`
      Post.create(${post}) {
          id,
          ts,
          headline
        }
      `;
      const document_result = await client.query(document_query);
      console.log(document_result);
    
    } catch (error) {
      console.log(error)
    }
  2. Set the FAUNA_SECRET environment variable to the internal/bulletinBoard server secret:

    export FAUNA_SECRET=<savedSecret>
  3. Run the application:

    node coffeestore.mjs

    You should see the Post document response:

    {
      data: '{\n' +
        '  name: "Post",\n' +
        '  coll: Collection,\n' +
        '  ts: Time("2023-08-09T23:13:53.400Z"),\n' +
        '  indexes: {},\n' +
        '  constraints: []\n' +
        '}',
      static_type: 'CollectionDef',
      summary: '',
      txn_ts: 1691680909210000,
      stats: {
        compute_ops: 1,
        read_ops: 0,
        write_ops: 1,
        query_time_ms: 95,
        contention_retries: 0,
        storage_bytes_read: 1569,
        storage_bytes_write: 353,
        rate_limits_hit: []
      },
      schema_version: 1691676820650000
    }
    {
      data: {
        id: '372626144347815969',
        ts: TimeStub { isoString: '2023-08-09T23:13:53.520Z' },
        headline: 'Lost Doggo'
      },
      static_type: '{ headline: Any, ts: Any, id: Any }',
      summary: '',
      txn_ts: 1691622833520000,
      stats: {
        compute_ops: 1,
        read_ops: 0,
        write_ops: 1,
        query_time_ms: 71,
        contention_retries: 0,
        storage_bytes_read: 0,
        storage_bytes_write: 213,
        rate_limits_hit: []
      },
      schema_version: 1691622833400000
    }
  4. In the Dashboard, navigate to your new Post collection.

Is this article helpful? 

Tell Fauna how the article can be improved:
Visit Fauna's forums or email docs@fauna.com

Thank you for your feedback!