SnippetStorageRLSNext.js
File upload with Storage
Create a private bucket (dashboard or a server client with a secret key), then add policies on storage.objects so each user only touches the folder named after their id. The Server Component lists the files for the first paint.
localhost:3000/documents
Documents
Private files, only visible to you.
- report.pdf248 KB
- logo.png36 KB
- invoice-march.pdf112 KB
Stored under documents/<your user id>/. Links are signed and expire after 60 s.
Preview · sample dataRendered from the code, no backend
'use client'import { useEffect, useState } from 'react'import type { FileEntry } from '@potalab/base'import { createBrowserClient } from '@potalab/base/next'
const base = () => createBrowserClient({ url: process.env.NEXT_PUBLIC_POTALAB_BASE_URL!, key: process.env.NEXT_PUBLIC_POTALAB_BASE_KEY! })
export function Documents({ userId, initialFiles }: { userId: string; initialFiles: FileEntry[] }) { const [files, setFiles] = useState(initialFiles) const [error, setError] = useState<string | null>(null) const [busy, setBusy] = useState(false) const [mounted, setMounted] = useState(false) useEffect(() => setMounted(true), []) if (!mounted) return <p>Loading...</p>
const bucket = base().storage.from('documents') // private bucket const refresh = async () => { const { data } = await bucket.list(`${userId}/`, { limit: 100 }) setFiles((data ?? []).filter((f) => f.id)) // folders have id: null }
async function onPick(e: React.ChangeEvent<HTMLInputElement>) { const file = e.target.files?.[0] if (!file) return setBusy(true) // Bytes go straight to object storage; the path must start with your user id (storage policy) const { error } = await bucket.upload(`${userId}/${file.name}`, file, { contentType: file.type, upsert: true, }) setError(error?.message ?? null) setBusy(false) if (!error) await refresh() }
async function share(name: string) { // Private bucket: short-lived signed URL (60 s) const { data } = await bucket.createSignedUrl(`${userId}/${name}`, 60) if (data) window.open(data.signedUrl, '_blank') }
async function remove(name: string) { await bucket.remove([`${userId}/${name}`]) await refresh() }
return ( <div> <label> {busy ? 'Uploading...' : 'Choose a file to upload'} <input type="file" onChange={onPick} disabled={busy} /> </label> {error && <p role="alert">{error}</p>}
<ul> {files.map((f) => ( <li key={f.name}> <span>{f.name}</span> <span>{f.size != null ? `${Math.round(f.size / 1024)} KB` : ''}</span> <button onClick={() => share(f.name)}>Get link</button> <button onClick={() => remove(f.name)}>Delete</button> </li> ))} </ul> </div> )}// app/documents/page.tsx (Server Component)import { redirect } from 'next/navigation'import { baseServer } from '@/lib/base-server'import { Documents } from './Documents'
export default async function DocumentsPage() { const base = await baseServer() const { data: { user } } = await base.auth.getUser() if (!user) redirect('/login')
// List this user's folder on the server: the first paint already has the files const { data: files } = await base.storage.from('documents').list(`${user.id}/`, { limit: 100 })
return ( <main> <h1>Documents</h1> <p>Private files, only visible to you.</p> <Documents userId={user.id} initialFiles={(files ?? []).filter((f) => f.id)} /> </main> )}// lib/base-server.ts// One client PER REQUEST: it holds the signed-in user's session.import { cookies } from 'next/headers'import { createServerClient } from '@potalab/base/next'
export async function baseServer() { const store = await cookies() return createServerClient({ url: process.env.NEXT_PUBLIC_POTALAB_BASE_URL!, key: process.env.NEXT_PUBLIC_POTALAB_BASE_KEY!, // lb_pub_... cookies: { getAll: () => store.getAll(), setAll: (list) => list.forEach((c) => store.set(c.name, c.value, c.options)), }, })}-- Bucket "documents" is created in the dashboard (private).-- Each user may only touch the folder named after their id.create policy documents_insert on storage.objects for insert to authenticated with check (bucket_id = 'documents' and owner_id = (select auth.uid()) and (storage.foldername(path))[1] = (select auth.uid())::text);create policy documents_select on storage.objects for select to authenticated using (bucket_id = 'documents' and owner_id = (select auth.uid()));create policy documents_update on storage.objects for update to authenticated using (bucket_id = 'documents' and owner_id = (select auth.uid()));create policy documents_delete on storage.objects for delete to authenticated using (bucket_id = 'documents' and owner_id = (select auth.uid()));Snippet for illustration, written for the Next.js App Router with @potalab/base/next,@potalab/base/react and @potalab/base. Set NEXT_PUBLIC_POTALAB_BASE_URL andNEXT_PUBLIC_POTALAB_BASE_KEY, adapt names and redirect URLs, and review policies before production.
Next: Realtime chat →