Pick your app

The examples below will be updated with your app ID.

Platform features

Storage

Instant Storage makes it simple to upload and serve files for your app. You can use Storage to store images, videos, documents, and any other file type.

Storage is still in beta, but you can request access here!

Uploading files

We use the db.storage.upload(pathname: string, file: File) function to upload a file.

async function upload(files: FileList) {
  const file = files[0];
  // use the file's current name as the path
  await db.storage.upload(file.name, file);
  // or, give the file a custom name
  await db.storage.upload('demo.png', file);
  // or, put it in the `images` subdirectory
  await db.storage.upload('images/demo.png', file);
  // or, put it in a subdirectory for the current user,
  // and restrict access to this file via Storage permissions
  await db.storage.upload(`${currentUser.id}/demo.png`, file);
}

return <input type="file" onChange={(e) => upload(e.target.files)} />;

The pathname determines where the file will be stored, and can be used with permissions to restrict access to certain files.

The file should be a File type, which will likely come from a file-type input.

Note that if the pathname already exists in your storage directory, it will be overwritten!

You may want to include some kind of unique identifier or timestamp in your pathname to ensure this doesn't happen.

Retrieving files

To retrieve a file URL, we use the db.storage.getDownloadUrl(pathname: string) function.

This function returns a signed URL that will be valid for 7 days.

This is important to keep in mind in cases where you want to save this URL somewhere, as demonstrated below in Caching the URL.

const [imageUrl, setImageUrl] = React.useState<string | null>(null);

React.useEffect(() => {
  db.storage
    .getDownloadUrl('images/demo.png')
    .then((signedUrl) => setImageUrl(signedUrl))
    .catch((err) => console.error('Failed to get file URL', err));
}, []);

return <img src={imageUrl} />;

Caching the URL

You might also want to cache the URL after retrieving it, in order to avoid calling getDownloadUrl every time you refresh the page.

Let's imagine you have an images namespace you use to store the file metadata of your images. You can use this to keep track of the expiration time of all your file URLs, and then refresh them accordingly.

// Simple component to upload and display image files
function App() {
  const { data } = db.useQuery({ images: {} });

  const upload = async (files: FileList) => {
    const file = files[0];
    const pathname = file.name; // or whatever custom file path you'd like
    const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days from now
    const isSuccess = await db.storage.upload(pathname, file);
    const cachedUrl = await db.storage.getDownloadUrl(pathname);

    db.transact(tx.images[id()].update({ cachedUrl, pathname, expiresAt }));
  };

  return (
    <div>
      <input type="file" onChange={(e) => upload(e.target.files)} />
      {data.images.map((image) => (
        <ImageViewer key={image.id} image={image} />
      ))}
    </div>
  );
}

Then, in your ImageViewer component, you can use the cachedUrl by default, and handle the expiration when necessary:

// Component to handle displaying the image URL and refreshing when necessary
function ImageViewer({ image }: { image: Schema.Image }) {
  const [imageUrl, setImageUrl] = React.useState(image.cachedUrl);

  React.useEffect(() => {
    // If the image URL has expired, refresh the signed url
    if (image.expiresAt < Date.now()) {
      const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000;

      db.storage.getDownloadUrl(image.pathname).then((url) => {
        // update the cached url
        db.transact(
          tx.images[image.id].update({
            cachedUrl: url,
            // reset expiration to 7 days from now
            expiresAt: expiresAt,
          })
        );

        setImageUrl(url);
      });
    }
  }, [image.expiresAt]);

  return <img src={imageUrl} />;
}

Permissions

At the moment, Storage permissions are handled in the same JSON settings as data permissions, using the special $files keyword.

To handle permissions for uploading files, we use the create action.

For downloading or viewing files, we use the view action.

By default, Storage permissions are disabled. This means that until you explicitly set permissions, no uploads or downloads will be possible.

In your permissions rules, you can use auth to access the currently authenticated user, and data to access the file metadata.

At the moment, the only available file metadata is data.path, which represents the file's path in Storage. (In the future we will likely include metadata such as size and type.)

Examples

Allow anyone to upload and retrieve files (not recommended):

{
  "$files": {
    "allow": {
      "view": "true",
      "create": "true"
    }
  }
}

Allow all authenticated users to view and upload files:

{
  "$files": {
    "allow": {
      "view": "isLoggedIn",
      "create": "isLoggedIn"
    },
    "bind": ["isLoggedIn", "auth.id != null"]
  }
}

Authenticated users may only upload and view files from their own subdirectory:

{
  "$files": {
    "allow": {
      "view": "isOwner",
      "create": "isOwner"
    },
    "bind": ["isOwner", "data.path.startsWith(auth.id)"]
  }
}

Allow all authenticated users to view files, but users may only upload png/jpeg image files:

{
  "$files": {
    "allow": {
      "view": "auth.id != null",
      "create": "isImage"
    },
    "bind": [
      "isImage",
      "data.path.endsWith('.png') || data.path.endsWith('.jpeg')"
    ]
  }
}

Admin SDK

The Admin SDK offers the same API for managing storage on the server, plus a few extra convenience methods for scripting.

Uploading files

Once again, we use the db.storage.upload(pathname: string, file: Buffer) function to upload a file on the backend.

Note that unlike our browser SDK, the file argument must be a Buffer:

import fs from 'fs';

async function upload(filepath: string) {
  const buffer = fs.readFileSync(filepath);
  await db.storage.upload('images/demo.png', buffer);
  // you can also optionally specify the Content-Type header in the metadata
  await db.storage.upload('images/demo.png', buffer, {
    contentType: 'image/png',
  });
}

The pathname determines where the file will be stored, and can be used with permissions to restrict access to certain files.

The file should be a Buffer type.

Note that if the pathname already exists in your storage directory, it will be overwritten!

You may want to include some kind of unique identifier or timestamp in your pathname to ensure this doesn't happen.

Retrieving a file URL

To retrieve a file URL, we use the db.storage.getDownloadUrl(pathname: string) function.

This works exactly the same as our browser SDK.

const url = await db.storage.getDownloadUrl('images/demo.png');

Listing all your files

We also offer the db.storage.list() function to retrieve a list of all your files in storage.

This can be useful for scripting, if you'd like to manage your files programmatically.

const files = await db.storage.list();

Deleting files

There are two ways to delete files:

  • db.storage.delete(pathname: string)
  • db.storage.deleteMany(pathnames: string[])

These allow you to either delete a single file, or bulk delete multiple files at a time.

These functions will permanently delete files from storage, so use with extreme caution!

const filename = 'demo.txt';
await db.storage.delete(filename);

const images = ['images/1.png', 'images/2.png', 'images/3.png'];
await db.storage.deleteMany(images);