@niledatabase/server

Installation and configuration

Install the SDK:

nodeyarn
npm install @niledatabase/server

Set an .env file with database credentials

NILEDB_USER=
NILEDB_PASSWORD=

Then import the SDK:

import Nile from "@niledatabase/server";
const nile = await Nile();

This creates an instance of Nile Server, which allows you to interact with Nile APIs and DB.

Configuration

If you would like to pass a configuration manually, that can be done by passing the config to Nile. Configs passed in this way take precedence over .env vars.

PropertyType.env varDescription
user
string
NILEDB_USERRequired. Username for database authentication.
password
string
NILEDB_PASSWORDRequired. Password for database authentication.
databaseId
string
NILEDB_IDID of the database.
databaseName
string
NILEDB_NAMEName of the database.
tenantId
string
NILEDB_TENANTID of the tenant associated.
userId
string
ID of the user associated.
db
PoolConfig

Configuration object for pg.Pool.

api
object
Configuration object for API settings.
api.basePath
string
NILEDB_API

Base host for API for a specific region. Default is api.thenile.dev.

api.cookieKey
string

Key for API cookie. Default is token.

api.token
string
NILEDB_TOKENToken for API authentication. Mostly for debugging.
debug
boolean
Flag for enabling debug logging.

Virtual Tenant Databases

In order to connect to Nile's virtual tenant databases and enjoy the full isolation and security, you need to get a reference to tenant-specific instance of Nile Server. This function will either create a new client connection based on the config, or return the existing instance if it has already been registered.

nile.getInstance({
  tenantId: tenantId,
  userId: userId,
  api: {
    token: token,
  },
});

userId and token are usually obtained from a cookie that is set during authentication. tenantId is the id of the tenant you want to connect to, and can be set and obtained in the path parameters, query parameters, or headers of the request. In our examples, we use path parameters for the tenant id. Handling of both cookies and path parameters is framework-specific.

nextjsexpress
import { cookies } from 'next/headers';

export default async function Page({ params }: { params: { tenantid: string } }) {

  // First, initialize global Nile instance if needed
  const nile = await Nile()

  const parsedCookies = JSON.parse(String(cookies().get('authData')?.value));
  const tenantNile = nile.getInstance({
    tenantId: params.tenantid,
    userId: parsedCookies.tokenData.sub,
    api: {
      token: parsedCookies.accessToken,
    }
  })
}

When you work with tenant-specific references, you can use the same APIs as with the global instance, but they will be scoped to the tenant and user you specified. Behind the scenes, the Nile SDK manages the connections to the tenant-specific database, and the authentication to the API.

You can leave tenantId, userId or token as undefined - in that case, the SDK will use the default configuration. In our examples, after a user logs in and before they pick a tenant, we set the userId and token, but leave tenantId as undefined. This lets us call the createTenant API as the authicated user.

nile.getInstance({
  tenantId: undefined,
  userId: parsedCookies.tokenData.sub,
  api: {
    token: parsedCookies.accessToken,
  },
});

APIs

Overview

Nile SDK provides a set of APIs that you can use to interact with Nile. The inputs and outputs of these APIs are all generated from the OpenAPI spec. You can find the latest spec here. The API is documented in the API Reference.

All inputs are extensions of Request type, and all outputs are extensions of Response type. This allows us to pass requests from the browser directly to Nile and return the response to the browser.

This is a valid API handler in NextJS:

export async function POST(req: Request) {
  return await api.auth.login(req);
}

However, you should examine the response and handle errors appropriately.

Error handling

When Nile returns an error response (40X or 500), the response body will be text, not JSON and contain the error message. Therefore you need to either check the status code before calling response.json or handle the exception that response.json will throw when called on an error response.

Most APIs will respond with 401 Unauthorized if nile.token is unset, invalid, not signed by Nile, or expired. The exception are login and signup APIs which don't require a token.

An example of handling errors can be something like this. We use create tenant as an example, but this applies to all APIs:

try {
    const createTenantResponse = await nile.api.tenants.createTenant({
      name: name,
    });
    if (tenantResponse.status === 401) {
      return res.status(401).json({
        message: "Unauthorized. Please log in again.",
      });
    }
    const tenant = await createTenantResponse.json();
    res.json(tenant);
  } catch (error: any) {
    console.log("error creating tenant: " + error.message);
    res.status(500).json({
      message: "Internal Server Error",
    });
}

Create Tenant

You call createTenant with the name of the tenant you want to create. This will create a tenant, and the current user (based on nile.token or nile.api.token) will be a member of that tenant.

const createTenantResponse = await nile.api.tenants.createTenant({
  name: name,
});
const tenant = await createTenantResponse.json();
res.json(tenant);

Get Tenant

This API call doesn't need any input parameters because it uses the tenant ID from the context and returns the current tenant.

const tenantResponse = await nile.api.tenants.getTenant();
const tenant = await tenantResponse.json();
res.json(tenant);

User signup

Signing up a user is a two-step process. First, you call signUp with the email and password of the user you want to create, and optionally a user name:

app.post("/api/sign-up", async (req, res) => {
  const resp = await nile.api.auth.signUp({
    email: req.body.email,
    password: req.body.password,
    preferredName: req.body.preferredName, // optional
  });
});

When the request succeeds, the response will include a JWT token that you can then use for this user. Usually, we set this token in a cookie and include it in the response to the browser:

const body = await resp.json();
const accessToken = body.token.jwt;
const decodedJWT = jwtDecode(accessToken);
const cookieData = {
  accessToken: accessToken,
  tokenData: decodedJWT,
};
res.cookie("authData", JSON.stringify(cookieData), {
  secure: process.env.NODE_ENV !== "development",
});
res.status(resp.status).json(JSON.stringify(body));

User login

User login is nearly identical to signup, but you only need to provide the email and password.

app.post("/api/login", async (req, res) => {
  const resp = await nile.api.auth.login({
    email: req.body.email,
    password: req.body.password,
  });
});

Similarly, if the login request succeeds, we should set the token in a cookie and include it in the response to the browser:

const body = await resp.json();
const accessToken = body.token.jwt;
const decodedJWT = jwtDecode(accessToken);
const cookieData = {
  accessToken: accessToken,
  tokenData: decodedJWT,
};
res.cookie("authData", JSON.stringify(cookieData), {
  secure: process.env.NODE_ENV !== "development",
});
res.status(resp.status).json(JSON.stringify(body));

Note that we are returning a response to the browser and expect it to redirect to the correct post-login page, based on whether login succeeds or fails.

Query Builder

Nile SDK includes a query builder and a connection pool that were designed to work with the Nile's virtual tenant databases with minimal overhead. You can use Nile with any ORM or database client that you prefer, but using the query builder will give you the best developer experience.

The query builder is built on top of pg-node, so the documentation for pg-node applies to Nile's query builder as well.

Connecting to the database

When you initialize the Nile Server object, you can pass any valid pg-node pool configuration object as the db parameter. One special case is the afterCreate method, which will configure the context of the pool connections for a specific tenantId and userId if they are provided.

For example:

import Nile from "@niledatabase/server";
export const nile = await Nile({
  user: process.env.NILEDB_USER,
  password: process.env.NILEDB_PASSWORD,
});

Because Nile manages the connection pool, we recommend not overriding the pool settings unless you are sure you know what you are doing.

Querying the database

The query builder is available as nile.db. You can use it to query the database directly, or to build a model layer on top of it. It works exactly like pg-node.

For example, to get all tenants that the current user is a member of:

const tenants = await nile.db.query(
  `SELECT 
  id, name 
  FROM 
  tenants JOIN users.tenant_users ON tenants.id = tenant_users.id 
  WHERE tenant_users.id = $1`,
  [nile.userId]
);

In order to query a virtual tenant database, you need to use a reference that you obtained from nile.getInstance with the current tenantId, userId and token.

For example, the following query will all rows in todos table for the current tenant:

const todos = await nile.db.query("SELECT * from todos ORDER BY title");

This also applies to inserts, updates and deletes. For example, to update a todo:

const { id, complete } = req.body;
await nile.db.query("UPDATE todos SET complete = $1 WHERE id = $2", [
  complete,
  id,
]);

and to create a new one:

const { title, complete } = req.body;
const newTodo = await nile.query(
  `INSERT INTO todos (title, complete, tenant_id) 
  VALUES ($1, $2, $3) 
  RETURNING *`,
  [title, Boolean(complete), nile.tenantId]
);