Auth Persistence Layer (APL)
Auth Persistence Layer (APL) is a technology-agnostic interface for managing auth data of registered Apps. APL provides a common set of operations that can be used by your application and App SDK.
The following doc contains a JavaScript / TypeScript implementation used by official Saleor apps.
The idea of APLs is to abstract specific persistent storage (like Redis, Upstash, or even a file) and provide a common interface for CRUD operations. This way, you can easily switch between different storage solutions without changing the application code. Additionally, app doesn't have to know any platform-specific details about the storage, like connection strings or endpoints.
During development we recommend using FileAPL
for fast and easy access on the local machine. For production you need to set up some form of database.
Hint: Specifics of APL is rarely write, but frequent read - it will be checked on every request to the app. We recommend to set up a database with fast read operations, like DynamoDB or Redis.
To increase the security, we recommend to store token
encrypted.
AuthData
Interface containing data used for communication with the Saleor API:
export interface AuthData {
// Token that Saleor provides to the app during installation. App must store it securely.
token: string;
// URL of Saleor GraphQL API, ending with /graphql/. Allows to identify the Saleor instance, especially in multi-tenant apps.
saleorApiUrl: string;
// ID of the app stored by Saleor in the database. It's unique for each installation. When app is reinstalled, ID will be different.
appId: string;
// Cached JWKS (JSON Web Key Set) used for webhook validation. It's available at https://<your-saleor-domain>/.well-known/jwks.json
jwks: string;
}
Available methods
-
get: (saleorApiUrl: string) => Promise<AuthData | undefined>
- If the entry for given saleorApiUrl exists, returns AuthData object. -
set: (authData: AuthData) => Promise<void>
- Save auth data. -
delete: (saleorApiUrl: string) => Promise<void>
- Remove auth data from the given API URL. -
getAll: () => Promise<AuthData[]>
- Returns all auth data available. -
isReady?: () => Promise<AplReadyResult>
- Optional: Check if the persistence layer behind APL is ready. For example, when a database connection was established. -
isConfigured?: () => Promise<AplConfiguredResult>
- Optional: Check if the persistence layer behind APL is configured. For example, when an env variable is required by the database connection.
AplReadyResult & AplConfiguredResult
Responses from isReady()
and isConfigured()
should match following:
type AplReadyResult =
| {
ready: true;
}
| {
ready: false;
error: Error;
};
type AplConfiguredResult =
| {
configured: true;
}
| {
configured: false;
error: Error;
};
Implementing these functions is optional, but it can be useful for handling asynchronous operations like database connection.
Example implementation
Let's create an APL which uses Redis for data storage:
import { createClient } from "redis";
import { APL, AuthData } from "@saleor/app-sdk/apl";
const client = createClient();
await client.connect();
/**
* The APL uses API URL as a key to store and retrieve AuthData.
* If you intend to use the same Redis instance for multiple Apps,
* add prefix to the keys to avoid overwriting the data by different apps.
* Keys will be formatted as below:
* - `APPID1:https://other-example.saleor.cloud/graphql`
* - `APPID1:https://example.saleor.cloud/graphql/`
* - `APPID2:https://example.saleor.cloud/graphql/`
**/
const prepareAuthDataKey = (apiUrl: string) => `${APP_ID}:${apiUrl}`;
const redisAPL: APL = {
get: async (saleorApiUrl: string) => {
const response = await client.get(prepareAuthDataKey(saleorApiUrl));
if (response) {
return JSON.parse(response);
}
return;
},
set: async (authData: AuthData) => {
await client.set(
prepareAuthDataKey(authData.saleorApiUrl),
JSON.stringify(authData)
);
},
delete: async (saleorApiUrl: string) => {
await client.del(prepareAuthDataKey(saleorApiUrl));
},
getAll: async () => {
throw new Exception("Not implemented.");
},
};
You'll be able to use it directly:
import { redisAPL } from "./apl";
const getSavedAuthData = async () => {
await redisAPL.get("https://example.saleor.cloud/graphql/");
};
Or access it from the context of API helpers from the SDK:
Hint: You don't need to write RedisAPL on your own, you can import it from the SDK:
import { RedisAPL } from "@saleor/app-sdk/APL/redis";
Using different APL depending on the environment
Depending on the environment your app is working on, you may want to use a different APL. For example, you may like to use FileAPL
during local development because it does not require any additional infrastructure. Deployed apps, on the other hand, need a more robust solution.
To handle both scenarios, initialize the proper APLs in your code based on its environment. In your application code:
// lib/saleorApp.ts
import { FileAPL } from "@saleor/app-sdk/APL/file";
import { UpstashAPL } from "@saleor/app-sdk/APL/upstash";
// Based on the environment variable, the app will use a different APL:
// - For local development store auth data in the `.auth-data.json`.
// - For app deployment on hosted environment like Vercel, use UpstashAPL
export const apl =
process.env.APL === "upstash" ? new UpstashAPL() : new FileAPL();
Now you can use it for in your view:
import { SALEOR_API_URL_HEADER } from "@saleor/app-sdk/const";
import { NextApiHandler } from "next";
// import created APL
import { apl } from "@lib/saleorApp";
const handler: NextApiHandler = async (request, response) => {
const saleorApiUrl = request.headers[SALEOR_API_URL_HEADER];
// Get auth data
const authData = apl.get(saleorApiUrl);
// view logic...
return response.status(200).end();
};
export default handler;
Available APLs
saleor/app-sdk
exports some built-in implementations that are using popular platforms for easier bootstrap.
FileAPL
File-based storage of auth data intended for local development. Data are stored in the .saleor-app-auth.json
file. You'll be able to develop an app without additional dependencies or infrastructure.
Please note: this APL supports single tenant only (new registrations overwrite previous ones) and should not be used on production.
UpstashAPL
Upstash is a Redis SaaS targeting serverless applications. Its free tier is more than enough to start developing multi-tenant Saleor Apps, and credit card info is not required to create an account.
APL implementation uses REST endpoints to store the data, so no extra dependencies are required.
To use UpstashAPL, you'll need an endpoint URL and password. After creating a database, both will be available at dashboard. Credentials can be passed to APL during its creation:
import { UpstashAPL } from "@saleor/app-sdk/APL";
const apl = new UpstashAPL({
restToken: "XXXXXXXXXXXXXX",
restURL: "https://eu2-red-panda-00000.upstash.io",
});
Or using environment variables: UPSTASH_TOKEN
, UPSTASH_URL
.
Note: Recently, Vercel introduced KV storage, which uses Upstash under the hood. Upstash APL will work with values provided by Vercel KV Storage too.
EnvAPL
For static & single-tenant applications, you may want to set everything once, usually in env variables, and leave it.
There is an APL for this use case: EnvAPL
.
Warning Using this APL is highly discouraged in any production environment. It will break if the app token is regenerated. It will not work with any flow related to updates of the app.
Usage
1. Configuring the app to use env apl
import { EnvAPL } from "@saleor/app-sdk/APL";
const apl = new EnvAPL({
env: {
/**
* Map your env variables here. You don't have these values yet
*/
token: envVars.SALEOR_APP_TOKEN,
appId: envVars.SALEOR_APP_ID,
saleorApiUrl: envVars.SALEOR_API_URL,
},
/**
* Set it to "true" - check your app logs during app registration. APL will print the values you need
*/
printAuthDataOnRegister: true,
});
2. Setting env variables
After step 1, you should see your logs in a similar way:
┌──────────────┬─────────────────────────────────────────────┐
│ (index) │ Values │
├──────────────┼─────────────────────────────────────────────┤
│ saleorApiUrl │ 'https://my-saleor-instance.cloud/graphql/' │
│ appId │ 'app-id' │
│ token │ 'some-token' │
│ jwks │ '{}' │
│ domain │ 'my-saleor-instance.cloud' │
└──────────────┴─────────────────────────────────────────────┘
You need to set env variables in your provider, e.g., in Vercel, you need appId
, token
, and saleorApiUrl
.
Ensure the names of the variables match the constructor options from Step 1.
3. Turning off logging and redeploying
After setting the env var, you should disable printing it for security reasons.
Change the constructor to include:
printAuthDataOnRegister: false
Then, redeploy the app. It should be configured and work for this specific Saleor instance.
Keep in mind that:
- If you promote the environment, you need to update
saleorApiUrl
- If you reinstall the app, you need to repeat the process to receive a new token and ID
VercelKvApl
Introduced in app-sdk 0.51.0
VercelKV is a key-value, Redis based distributed database that Vercel provides in their platform. As an APL it can be used both for single and multi-tenant approach.
To configure the APL in the app:
1: Install vercel/kv
SDK
todo
2: Import APL from @saleor/app-sdk
import { VercelKvApl } from "@saleor/app-sdk/APL/vercel-kv";
export const apl = new VercelKvApl()
3: Set environment variables
# Provided automatically by Vercel
KV_URL=
KV_REST_API_URL=
KV_REST_API_TOKEN=
KV_REST_API_READ_ONLY_TOKEN=
# A key to the Redis collection. All APL items will be stored inside this collection
KV_STORAGE_NAMESPACE=
RedisAPL
RedisAPL requires redis
client to be installed.
Similar to VercelKV (which is Redis too), all data is stored in the hash collection. You can provide a custom key for the collection in the constructor
import { createClient } from 'redis';
import { RedisAPL } from '@saleor/app-sdk/APL/redis';
const apl = new RedisAPL({
client: createClient(),
hashCollectionKey: 'my-key', // optional, by default "saleor_app_auth"
});
See Redis documentation for more details on how to set up the client.
SaleorCloudAPL
You may see that there is a SaleorCloudAPL
exported as well. This APL is specific implementation that Saleor Cloud uses when hosting apps on the cloud platform.
It's not available to use and will not be bundled by your app if not imported.