# Storage

How to upload and serve files with Instant.

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

## Storage quick start

Let's build a full example of how to upload and display a grid of images

```shell 
npx create-next-app instant-storage --tailwind --yes
cd instant-storage
npm i @instantdb/react
```

Initialize your schema and permissions via the [cli tool](/docs/cli)

```
npx instant-cli@latest init
```

Now open `instant.schema.ts` and replace the contents with the following code.

```javascript 
import { i } from "@instantdb/react";

const _schema = i.schema({
  entities: {
    $files: i.entity({
      path: i.string().unique().indexed(),
      url: i.string(),
    }),
    $users: i.entity({
      email: i.string().unique().indexed(),
    }),
  },
  links: {},
  rooms: {},
});

// This helps TypeScript display nicer intellisense
type _AppSchema = typeof _schema;
interface AppSchema extends _AppSchema {}
const schema: AppSchema = _schema;

export type { AppSchema };
export default schema;
```

Similarly open `instant.perms.ts` and replace the contents with the following

```javascript 
import type { InstantRules } from "@instantdb/react";

// Not recommended for production since this allows anyone to
// upload/delete, but good for getting started
const rules = {
  "$files": {
    "allow": {
      "view": "true",
      "create": "true",
      "delete": "true"
    }
  }
} satisfies InstantRules;

export default rules;
```

Push up both the schema and permissions to your Instant app with the following command

```shell 
npx instant-cli@latest push
```

And then replace the contents of `app/page.tsx` with the following code.

```javascript 
'use client';

import { init, InstaQLEntity } from '@instantdb/react';
import schema, { AppSchema } from '../instant.schema';
import React from 'react';

type InstantFile = InstaQLEntity<AppSchema, '$files'>

const APP_ID = process.env.NEXT_PUBLIC_INSTANT_APP_ID;

const db = init({ appId: APP_ID, schema });

// `uploadFile` is what we use to do the actual upload!
// the `$files` will automatically update once the upload is complete
async function uploadImage(file: File) {
  try {
    // Optional metadata you can set for uploads
    const opts = {
      // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
      // Default: 'application/octet-stream'
      contentType: file.type,
      // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
      // Default: 'inline'
      contentDisposition: 'attachment',
    };
    await db.storage.uploadFile(file.name, file, opts);
  } catch (error) {
    console.error('Error uploading image:', error);
  }
}

function App() {
  // $files is the special namespace for querying storage data
  const { isLoading, error, data } = db.useQuery({
    $files: {
      $: {
        order: { serverCreatedAt: 'asc' },
      },
    },
  });

  if (isLoading) {
    return null;
  }

  if (error) {
    return <div>Error fetching data: {error.message}</div>;
  }

  // The result of a $files query will contain objects with
  // metadata and a download URL you can use for serving files!
  const { $files: images } = data
  return (
    <div className="box-border bg-gray-50 font-mono min-h-screen p-5 flex items-center flex-col">
      <div className="tracking-wider text-5xl text-gray-300 mb-8">
        Image Feed
      </div>
      <ImageUpload />
      <div className="text-xs text-center py-4">
        Upload some images and they will appear below! Open another tab and see
        the changes in real-time!
      </div>
      <ImageGrid images={images} />
    </div>
  );
}

interface SelectedFile {
  file: File;
  previewURL: string;
}

function ImageUpload() {
  const [selectedFile, setSelectedFile] = React.useState<SelectedFile | null>(null);
  const [isUploading, setIsUploading] = React.useState(false);
  const fileInputRef = React.useRef<HTMLInputElement>(null);
  const { previewURL } = selectedFile || {};

  const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      const previewURL = URL.createObjectURL(file);
      setSelectedFile({ file, previewURL });
    }
  };

  const handleUpload = async () => {
    if (selectedFile) {
      setIsUploading(true);

      await uploadImage(selectedFile.file);

      URL.revokeObjectURL(selectedFile.previewURL);
      setSelectedFile(null);
      fileInputRef.current?.value && (fileInputRef.current.value = '');
      setIsUploading(false);
    }
  };

  return (
    <div className="mb-8 p-5 border-2 border-dashed border-gray-300 rounded-lg">
      <input
        ref={fileInputRef}
        type="file"
        accept="image/*"
        onChange={handleFileSelect}
        className="font-mono"
      />
      {isUploading ? (
        <div className="mt-5 flex flex-col items-center">
          <div className="w-8 h-8 border-2 border-t-2 border-gray-200 border-t-green-500 rounded-full animate-spin"></div>
          <p className="mt-2 text-sm text-gray-600">Uploading...</p>
        </div>
      ) : previewURL && (
        <div className="mt-5 flex flex-col items-center gap-3">
          <img src={previewURL} alt="Preview" className="max-w-xs max-h-xs object-contain" />
          <button onClick={handleUpload} className="py-2 px-4 bg-green-500 text-white border-none rounded-sm cursor-pointer font-mono">
            Upload Image
          </button>
        </div>
      )}
    </div>
  );
}

function ImageGrid({ images }: { images: InstantFile[] }) {
  // Use `db.transact` to delete files
  const handleDelete = async (image: InstantFile) => {
    db.transact(db.tx.$files[image.id].delete());
  }

  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5 w-full max-w-6xl">
      {images.map((image) => {
        return (
          <div key={image.id} className="border border-gray-300 rounded-lg overflow-hidden">
            <div className="relative">
              {/* $files entities come with a `url` property */}
              <img src={image.url} alt={image.path} className="w-full h-64 object-cover" />
            </div>

            <div className="p-3 flex justify-between items-center bg-white">
              <span>{image.path}</span>
              <span onClick={() => handleDelete(image)} className="cursor-pointer text-gray-300 px-1">
                𝘟
              </span>
            </div>
          </div>
        )
      })}
    </div>
  );
}

export default App;
```

With your schema, permissions, and application code set, you can now run your app!

```shell 
npm run dev
```

Go to `localhost:3000`, and you should see a simple image feed where you can
upload and delete images!

## Storage client SDK

Below you'll find a more detailed guide on how to use the Storage API from
react.

### Upload files

Use `db.storage.uploadFile(path, file, opts?)` to upload a file.

- `path` determines where the file will be stored, and can be used with permissions to restrict access to certain files.
- `file` should be a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) type, which will likely come from a [file-type input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file).
- `opts` can be used to set additional metadata like `contentType` and `contentDisposition`

```javascript
// use the file's current name as the path
await db.storage.uploadFile(file.name, file);

// or, give the file a custom name
const path = `${user.id}/avatar.png`;
await db.storage.uploadFile(path, file);

// or, set the content type and content disposition
const path = `${user.id}/orders/${orderId}.pdf`;
await db.storage.uploadFile(path, file, {
  contentType: 'application/pdf',
  contentDisposition: `attachment; filename="${orderId}-confirmation.pdf"`,
});
```

### Overwrite files

If the `path` already exists in your storage directory, it will be overwritten!

```javascript
// Uploads a file to 'demo.png'
await db.storage.uploadFile('demo.png', file);

// Overwrites the file at 'demo.png'
await db.storage.uploadFile('demo.png', file);
```

If you don't want to overwrite files, you'll need to ensure that each file has a unique path.

### View files

You can retrieve files by querying the `$files` namespace.

```javascript
// Fetch all files from earliest to latest upload
const query = {
  $files: {
    $: {
      order: { serverCreatedAt: 'asc' },
    },
  },
});
const { isLoading, error, data } = db.useQuery(query);
```

```javascript
console.log(data)
{
  "$files": [
    {
      "id": fileId,
      "path": "demo.png"
      // You can use this URL to serve the file
      "url": "https://instant-storage.s3.amazonaws.com/...",
      "content-type": "image/png",
      "content-disposition": "attachment; filename=\"demo.png\"",
    },
    // ...
  ]
}
```

You can use query filters and associations as you would with any other namespace
to filter and sort your files.

```javascript 
// instant.schema.ts
// ---------------
import { i } from '@instantdb/core';
const _schema = i.schema({
  entities: {
    $files: i.entity({
      path: i.string().unique().indexed(),
      url: i.string(),
    }),
    $users: i.entity({
      email: i.string().unique().indexed(),
    }),
    profiles: i.entity({
      nickname: i.string(),
      createdAt: i.date(),
    }),
  },
  links: {
    profileUser: {
      forward: { on: 'profiles', has: 'one', label: '$user' },
      reverse: { on: '$users', has: 'one', label: 'profile' },
    },
    profileUploads: {
      forward: { on: 'profiles', has: 'many', label: '$files' },
      reverse: { on: '$files', has: 'one', label: 'profile' },
    },
  },
});
```

```javascript 
// app/page.tsx
// ---------------
// Find files associated with a profile
const { user } = db.useAuth();
const query = {
  profiles: {
    $: {
      where: {"$user.id": user.id}
    },
    $files: {},
  },
});
// Defer until we've fetched the user and then query associated files
const { isLoading, error, data } = db.useQuery(user ? query : null);
```

### Delete files

Use `db.transact` to delete files.

```javascript
// Delete by id
db.transact(db.tx.$files[fileId].delete());

// Delete by path
db.transact(db.tx.$files[lookup('path', 'photos/demo.png')].delete());

// Delete multiple files
db.transact(fileIds.map((id) => db.tx.$files[id].delete()));
```

### Update files

You can use `db.transact` to update file paths, as well as any custom columns you've added to files.

So if you're schema looked like this:

```javascript
import { i } from "@instantdb/react";

const _schema = i.schema({
  entities: {
    $files: i.entity({
      path: i.string().unique().indexed(),
      isFavorite: i.boolean().optional()
      url: i.string(),
    }),
  },
});
```

You could run a transaction like this:

```javascript
// Move all files under 'documents/my-video-project/' to 'videos/my-video-project/' and make them favorites

const { data } = await db.query({
  $files: { $: { where: { path: { $like: 'documents/my-video-project/%' } } } },
});

await db.transact(
  data.$files.map((file) =>
    db.tx.$files[file.id].update({
      path: file.path.replace(
        'documents/my-video-project/',
        'videos/my-video-project/',
      ),
      isFavorite: true,
    }),
  ),
);
```

`path` is a unique attribute so if another file exists with that path, then the transaction
will fail.

At the moment we only allow updating the `path` attribute of `$files`, as well as any custom columns you've created. If you
try to update another attribute like `content-type` the transaction will fail.

### Link files

When the upload succeeds, `uploadFile` returns a `data` object containing a file ID associated with the uploaded file. You can use this id to link the file to other namespaces.

```javascript
async function uploadImage(file: File) {
  try {
    const path = `${user.id}/avatar`;
    const { data } = await db.storage.uploadFile(path, file);
    await db.transact(db.tx.profiles[profileId].link({ avatar: data.id }));
  } catch (error) {
    console.error('Error uploading image:', error);
  }
}
```

[Check out this repo](https://github.com/jsventures/instant-storage-avatar-example)
for a more detailed example showing how you may leverage links to implement an avatar upload feature

## Using Storage with React Native

The SDK expects a `File` object. In React Native the built-in `fetch` function can be used to construct a `File`, then you can pass that to the `uploadFile` method.

Example:

```typescript
import { init, InstaQLEntity } from '@instantdb/react-native';
import schema, { AppSchema } from '../instant.schema';
import * as FileSystem from 'expo-file-system';

const APP_ID = process.env.EXPO_PUBLIC_INSTANT_APP_ID;

const db = init({ appId: APP_ID, schema });

const localFilePath = 'file:///var/mobile/Containers/Data/my_file.m4a';

const fileInfo = await FileSystem.getInfoAsync(localFilePath);

if (!fileInfo.exists) {
  throw new Error(`File does not exist at path: ${localFilePath}`);
}

// Convert the local file to a File object
const res = await fetch(fileInfo.uri);
const blob = await res.blob();
const file = new File([blob], payload.recordingId, { type: 'audio/x-m4a' });

await db.storage.uploadFile('my_file.m4a', file);
```

## Storage admin SDK

The Admin SDK offers a similar API for managing storage on the server. Permission
checks are not enforced when using the Admin SDK, so you can use it to manage
files without worrying about authentication.

### Uploading files

`db.storage.uploadFile(path, file, opts?)` is also available to upload a file
on the backend. In the admin SDK, the `file` argument must either be a buffer
or a stream.

```tsx
import fs from 'fs';

const fp = 'path/to/your/file.png';
const dest = 'images/demo.png';

// Upload a file from a buffer
const buffer = fs.readFileSync(filepath);
const { data } = await db.storage.uploadFile(dest, buffer);

// Upload a file from a stream
// IMPORTANT: You must provide `fileSize` as an option when uploading via stream
const stream = fs.createReadStream(fp);
const fileSize = fs.statSync(fp).size;
const { data } = await db.storage.uploadFile(dest, stream, {
  contentType: contentType,
  fileSize,
});
```

### View Files

Retrieving files is similar to the client SDK, but we use `db.query()` instead
of `db.useQuery()`.

```ts
const query = {
  $files: {
    $: {
      order: { serverCreatedAt: 'asc' },
    },
  },
});
const data = db.query(query);
```

### Delete files

Use `db.transact` to delete files.

```ts
// Delete by id
await db.transact(db.tx.$files[fileId].delete());

// Delete by path
await db.transact(db.tx.$files[lookup('path', 'photos/demo.png')].delete());

// Delete multiple files
await db.transact(fileIds.map((id) => db.tx.$files[id].delete()));
```

### Link files

Similar to the client SDK, after uploading a file, you can use the response to
link the upload to other entities.

```typescript
// Assume we have a user ID and a buffer for the file
const { data } = await db.storage.uploadFile('images/demo.png', buffer);
db.transact([db.tx.$users[userId].link({ avatar: data.id })]);
```

## Permissions

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

- _create_ permissions enable uploading `$files`
- _view_ permissions enable viewing `$files`
- _update_ permissions enable updating `$files`
- _delete_ permissions enable deleting `$files`
- _view_ permissions on `$files` and _update_ permisssions on the forward entity enabling linking and unlinking `$files`

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. Here are some example permissions

Allow anyone to upload and retrieve files (easy to play with but not recommended for production):

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

Allow all authenticated users to view and upload files:

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

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

```json
{
  "$files": {
    "allow": {
      "view": "isOwner",
      "update": "isOwner",
      "create": "isOwner"
    },
    "bind": ["isOwner", "data.path.startsWith(auth.id + '/')"]
  }
}
```
