Storage
Store files in object storage. There are two access modes: public files served anonymously from a CDN, and private files reachable only with a short-lived token that you — the project owner — mint server-side. Clients never mint tokens; that keeps private files your responsibility.
ich_admin_), which only your server / Edge Functions hold. This is by design — a leaked anon key can't sign for or enumerate private files.Public files
Anything in the reserved public bucket is served anonymously from the CDN — no token. Put the URL straight into an <img> tag:
https://cdn.ichibase.net/<project>/public/<path>Make paths unguessable (UUIDs) if you want obscurity. Reads are CDN-cached and metered.
Private files: you mint the tokens
For everything else, the flow is: your client asks your backend (an Edge Function) for access → your function checks who the user is and what they may touch → it mints a token scoped to a single file or a whole folder, with an expiry → it returns the signed URL to the client. The token is reusable until it expires.
Deploy this as an Edge Function. It runs with your service key injected as env:
// Edge Function: "files" — your storage gateway. Runs server-side with the
// service key, so it (and only it) can mint tokens.
const BASE = Deno.env.get("ICHIBASE_PROJECT_URL")!;
const SERVICE = Deno.env.get("ICHIBASE_SERVICE_KEY")!; // ich_admin_… (never leaves the server)
Deno.serve(async (req) => {
// 1. Authenticate the caller however you like (their JWT, a session, etc.)
const userId = req.headers.get("x-user-id"); // example
if (!userId) return new Response("unauthorized", { status: 401 });
const { bucket, path } = await req.json();
// 2. Decide what this user may reach. Here: only their own folder.
if (!path.startsWith(`users/${userId}/`)) {
return new Response("forbidden", { status: 403 });
}
// 3. Mint a READ token (service key required). Single file:
const res = await fetch(`${BASE}/storage/get-url`, {
method: "POST",
headers: {
"Authorization": `Bearer ${SERVICE}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ bucket, path, ttl_seconds: 300 }),
});
const signed = await res.json(); // { url, token, exp }
return Response.json(signed);
});Folder (prefix) tokens
Pass recursive: true with a folder path and the token authorizes everything under that folder until it expires. The response is a base URL + a token; the client builds ${base}/<file>?token=${token} for each file.
// inside your Edge Function — a token good for the whole folder for 1 hour
const res = await fetch(`${BASE}/storage/get-url`, {
method: "POST",
headers: { "Authorization": `Bearer ${SERVICE}`, "Content-Type": "application/json" },
body: JSON.stringify({
bucket: "invoices",
path: `users/${userId}/`, // a folder
recursive: true,
ttl_seconds: 3600,
}),
});
const { base, token } = await res.json();
// client reads any file under it: `${base}/march.pdf?token=${token}`Uploads — you sign the PUT URL
Same pattern for writes: your function decides if the user may upload, then signs a one-time PUT URL and returns it. The client PUTs the bytes straight to storage; your server never proxies the file.
// inside your Edge Function — sign an upload URL
const res = await fetch(`${BASE}/storage/get-put-url`, {
method: "POST",
headers: { "Authorization": `Bearer ${SERVICE}`, "Content-Type": "application/json" },
body: JSON.stringify({
bucket: "avatars",
path: `users/${userId}/avatar.png`,
content_type: "image/png",
content_length: 102400,
}),
});
const { url } = await res.json();
return Response.json({ url }); // client does: fetch(url, { method: "PUT", body: file })Deletes work the same way — your function calls /storage/deletewith the service key after authorizing the request. One “files” function can route GET / PUT / DELETE and hold all your token logic in one place.
On the client
The client just calls your function and uses what it gets back — no storage SDK, no keys beyond the anon key it already has. A private read is the same URL as a public one, plus a ?token= query parameter carrying the JWT you minted:
https://cdn.ichibase.net/<project>/<bucket>/<path>?token=<jwt>For a single file, /storage/get-url returns a ready-built url with the ?token= already appended — use it as-is. For a folder (prefix) token you get a base + token back and append the ?token= parameter yourself for each file:
// ask your Edge Function for access, then read with the token as a query param.
// invoke() attaches the signed-in user's token automatically.
const { data } = await ichi.functions.invoke('files', {
body: { bucket: 'invoices', path: `users/${myUserId}/march.pdf` },
});
// single file → data.url already ends with ?token=…
const file = await fetch(data.url);
// folder (prefix) token → build the URL with the token parameter yourself
// const { base, token } = data;
// const file = await fetch(`${base}/march.pdf?token=${token}`);