Platform features
Storage
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
npx create-next-app instant-storage --tailwind --yes cd instant-storage npm i @instantdb/react
Initialize your schema and permissions via the cli tool
npx instant-cli@latest init
Now open instant.shema.ts
and replace the contents with the following code.
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
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
npx instant-cli@latest push
And then replace the contents of app/page.tsx
with the following code.
'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); } } // `delete` is what we use to delete a file from storage // `$files` will automatically update once the delete is complete async function deleteImage(image: InstantFile) { await db.storage.delete(image.path); } 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 cursor-pointer font-mono"> Upload Image </button> </div> )} </div> ); } function ImageGrid({ images }: { images: InstantFile[] }) { const [deletingIds, setDeletingIds] = React.useState<Set<string>>(new Set()); const handleDelete = async (image: InstantFile) => { setDeletingIds((prev) => new Set([...prev, image.id])); await deleteImage(image); setDeletingIds((prev) => { prev.delete(image.id); return prev; }); } 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) => { const isDeleting = deletingIds.has(image.id); 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" /> {isDeleting && ( <div className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center"> <div className="w-8 h-8 border-2 border-t-2 border-gray-200 border-t-white rounded-full animate-spin"></div> </div> )} </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!
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 aFile
type, which will likely come from a file-type input.opts
is optional and can be used to set thecontentType
andcontentDisposition
headers for the file.
// 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', contentDisporition: `attachment; filename="${orderId}-confirmation.pdf"`, });
Overwrite files
If the path
already exists in your storage directory, it will be overwritten!
// 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.
// Fetch all files from earliest to latest upload const query = { $files: { $: { order: { serverCreatedAt: 'asc' }, }, }, }); const { isLoading, error, data } = db.useQuery(query);
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.
// 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' }, }, }, });
// 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.storage.delete(path)
to delete a file.
// This will delete the file at 'demo.png' await db.storage.delete('demo.png');
Link files
Use links to associate files with other entities in your schema.
async function uploadImage(file: File) { try { // Create an explicit upload path const path = `${user.id}/avatar`; // Upload the file const { data } = await db.storage.uploadFile(path, file); // Link it to a profile await db.transact(tx.profiles[profileId].link({ avatar: data.id })); } catch (error) { console.error('Error uploading image:', error); } }
Similar to $users
, links on $files
can only be created in the reverse direction.
// instant.schema.ts // simplfied version 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({ createdAt: i.date().indexed(), nickname: i.string().unique().indexed(), }), }, links: { profiles$user: { forward: { on: 'profiles', has: 'one', label: '$user', }, reverse: { on: '$users', has: 'one', label: 'profile', }, }, profilesAvatar: { forward: { on: 'profiles', has: 'one', label: 'avatar', }, // Notice that $files is on the reverse side reverse: { on: '$files', has: 'one', label: 'profile', }, }, }, rooms: {}, });
Check out this repo for a more detailed example showing how you may leverage links to implement an avatar upload feature
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
Once again, we use the db.storage.uploadFile(path, file, opts?)
function to upload a file on the backend.
Note that unlike our browser SDK, the file
argument must be a Buffer
. In this case you'll likely want to at least specify the contentType
in the options otherwise the default content-type will be application/octet-stream
.
import fs from 'fs'; async function upload(filepath: string) { const buffer = fs.readFileSync(filepath); await db.storage.upload('images/demo.png', buffer, { contentType: 'image/png', } }
View Files
Retrieving files is similar to the client SDK, but we use db.query()
instead of db.useQuery()
.
const query = { $files: { $: { order: { serverCreatedAt: 'asc' }, }, }, }); const data = db.query(query);
Delete files
There are two ways to delete files with the admin SDK:
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.
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);
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
- 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):
{ "$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 + '/')"] } }