Getting started
Your first internal app, in 30 seconds.
Forigi turns any HTML zip into a Microsoft-SSO-gated dashboard your team can open. Drop in a bundle, optionally wire it to a SharePoint or OneDrive file, and share the URL. This guide covers everything you need to ship your first app and the things worth knowing about how data flows through the platform.
Deploy your first app
Forigi accepts any static HTML/CSS/JavaScript zip with index.html at the root. There's no build step, no framework lock-in — anything that runs in a browser runs here.
Package your bundle
Make sure your folder structure has index.html at the top level:
my-dashboard.zip
├── index.html ← required, must be at root
├── styles.css
├── app.js
└── assets/
└── chart.svg- Up to 50 MB total
- Up to 1,000 files
- Per-file cap: 5 MB
- Allowed types: HTML, CSS, JavaScript, JSON, common image and font formats. No
.env, no executables, nonode_modules.
Upload via the dashboard
Drag your zip onto the Deploy a new app tile on the dashboard, or click it to open the deploy form. Give your app a name and an optional description. Forigi:
- Validates the zip (size, file types, paths)
- Injects the platform SDK so your bundle can call
window.platform.* - Generates a slug like
q1-sales-dashboard-a3f9 - Stores the bundle in encrypted object storage
- Hands you a URL like
app.forigi.com/apps/q1-sales-dashboard-a3f9
Open and share the URL
By default only you can open the URL. Anyone you grant access (see step 3) can sign in with their Microsoft account and view it. Each viewer gets their own session; the platform never serves your data to anyone else's identity.
Connect a data source
Most internal apps need data. Forigi reads SharePoint and OneDrive files using each viewer's own Microsoft permissions — not yours, not a service account. This is the load-bearing security promise: your IT department keeps the same control over data flows they already have through Microsoft 365.
Get a sharing link from OneDrive or SharePoint
In OneDrive or SharePoint, open your file. Click Share → Copy link. The URL looks like https://yourtenant-my.sharepoint.com/:x:/g/personal/….
Connect it in Forigi
Open your app's detail page (/dashboard/apps/<your-app>) → Connected data → Connect a file → paste the URL → name it (e.g. deals). Forigi resolves the link against your Microsoft 365 tenant, previews the first rows, and saves the wiring.
Pick a load mode:
Data is fetched once on page load and ready immediately. Best for small/mid datasets that don't change every minute.
window.DATA.deals // already populatedThe bundle fetches on-demand. Best for larger tables, filters, or anything that should reflect the latest state.
await platform.query("deals", {
where: { stage: "Negotiation" },
limit: 100,
})Important — how viewer permissions actually work
Read this before sharing data files
Microsoft has two parallel permission models on OneDrive-for-Business that look identical in the UI but behave differently:
- Sharing-link permissions("Anyone with the link", "People in <your tenant>") grant access via the URL itself, in the browser. Forigi reads files via Graph API, which doesn't honor link permissions.
- Per-user permissions (typing the viewer's email into the Share dialog) grant access to that person's account. Forigi can read.
For viewers to read your data, invite them to the file by email in OneDrive's Share dialog. Link permissions alone aren't enough.
If a viewer hits a file they don't have access to, the platform shows an in-page banner with a Request access button that opens Microsoft's native request flow. They click it, you see a request in OneDrive, you grant access — done. No manual user-management on Forigi's side.
Add a database for stateful apps
If your app needs to save data (notes, settings, anything users add and want back later), Forigi can provision a per-app Postgres schema in the platform database. Bundles use the provisioned tables via window.platform.db.<table>.
Declare a schema in your bundle
Include a forigi.json file at the root of your zip:
{
"version": 1,
"tables": {
"notes": {
"default_visibility": "public",
"columns": {
"title": { "type": "string", "indexed": true, "max_length": 200 },
"body": { "type": "string", "max_length": 10000 },
"is_pinned": { "type": "boolean", "default": false },
"private_note": { "type": "string", "sensitive": true }
}
}
}
}- Types:
string,number,boolean,date,json - indexed: true — Postgres indexes for fast filter/sort
- sensitive: true — encrypted at rest with a per-tenant key the platform manages and never logs
- default_visibility:
public(visible to all viewers) orprivate(visible only to the row's creator)
Approve the schema after deploy
Forigi never auto-applies a schema. After you deploy, the schema is staged and an amber banner appears on the app's database panel: "Schema waiting for review." Click Review → Apply to provision the Postgres tables. Same flow for additive changes (new columns) on subsequent deploys.
Destructive changes (renaming or removing columns) are refused at apply time. Drop the table explicitly first if you need a clean slate.
Use the database from your bundle
// list rows
const notes = await window.platform.db.notes.list();
// insert (system columns _id, _created_by_oid, _created_at,
// _visibility are stamped server-side; you don't set them)
const note = await window.platform.db.notes.insert({
title: "Morning standup",
body: "Discussed Q3 plan",
});
// update
await window.platform.db.notes.update(note._id, {
is_pinned: true,
});
// delete
await window.platform.db.notes.delete(note._id);Row-level access is automatic
- Every row gets
_created_by_oidstamped server-side from the viewer's Microsoft Object ID. Bundles cannot forge it. - New rows respect the table's
default_visibility; the bundle can override per-row. - Non-owner viewers see public rows + their own private rows. App owners and tenant admins see all rows.
sensitive: truecolumns are encrypted with a per-tenant key and decrypted only when read by an authorized viewer.
Full security model: the App Databases reference. Worth reading before your IT review.
Call an external API with connectors
Bundles can't fetch() arbitrary URLs — the Content Security Policy blocks it. The governed path is a connector: your IT admin registers an external HTTPS API once (base URL, encrypted credentials, an allowed-path pattern), and bundles call it by name through window.platform.fetchExternal(). The platform injects the credentials server-side (your bundle never sees them) and audit-logs every call.
Read from a connector (GET)
// Reads work as soon as the admin registers the connector
// and grants your app access.
const employees = await window.platform.fetchExternal(
"hr-employees",
"/employees?status=active",
);Write to a connector (POST / PUT / PATCH / DELETE)
Pass a third argument with method and an optional body (JSON-encoded for you):
const quote = await window.platform.fetchExternal(
"freight-rates",
"/rates/GetRates",
{ method: "POST", body: { origin: "LAX", dest: "JFK", weight: 500 } },
);Writes are off by default and need three admin actions, or the call throws METHOD_NOT_ALLOWED / CONNECTOR_NOT_GRANTED:
- Workspace switch Allow apps to write to external APIs in
/admin/settings - Per-connector Allow writes+ the specific verb in the connector's allowed methods on
/admin/connectors/<id> - An explicit app grant — writes are never auto-granted, even on self-serve connectors
Handle errors
fetchExternal throws an Error with a stable .code on failure. On UPSTREAM_4XX the upstream's response body is on err.body (truncated) — surface it so you can see what the external API actually said. The SDK auto-retries UPSTREAM_5XX once for idempotent verbs only (GET/PUT/DELETE); a POST/PATCH is never silently retried. Your IT admin manages connectors and their allowed verbs in /admin/connectors.
Poll an API on a schedule (no tab open)
A scheduled jobcalls a connector server-side on an interval and stores the response in an approved app-db table — your dashboard just reads the table. Configure jobs on the app's detail page, or let Claude do it over MCP (forigi_probe_connector → forigi_schedule_job; see the MCP guide). An API's response shape almost never matches your columns, so give the job a response mapping — a safe, declarative reshape (no code runs). A complete connector_fetch config with a POST body and a mapping:
{
"connector_name": "freight-rates",
"path": "/Rate/GetRates",
"method": "POST",
"body": { "origin": "LAX", "dest": "JFK", "weight": 500 },
"into_table": "quote_snapshots",
"mode": "append",
"mapping": {
"static": { "lane_id": "LAX-JFK", "source": "freight-api" },
"fields": {
"carrier_count": { "count": "CompositeRateQuote.0.Quotes" },
"ok": { "exists": "CompositeRateQuote.0.Quotes" },
"fetched_at": { "now": true }
},
"raw_into": "response"
}
}fieldsmaps each column to exactly one of{ path }(pluck a value),{ count }(array length),{ exists }(boolean),{ const }, or{ now: true }(run timestamp).rows_pathturns an array into one row per element; without it you get one summary row per run.- A mapping that doesn't match the response makes the job error visibly — it never silently inserts a blank row.
- Don't map the system columns (
_id,_created_at,_updated_at,_created_by_oid,_visibility) — they're stamped automatically on every row. - Scheduled jobs are off by default: IT enables them (and connector calls from schedules, and each connector's Allow scheduled jobs flag) in
/admin/settingsand/admin/connectors.
Troubleshooting
Permission denied when viewing data
Tenant pending approval
APP_DATABASES_DISABLED when calling window.platform.db
app_databases_allowed in /admin/settings. Default is OFF — IT must opt in per docs/APP_DATABASES.md.Access denied opening someone else's app URL
Microsoft sign-out doesn't immediately invalidate Forigi
What to read next
The IT-facing data sheet for the per-app Postgres feature. Read before flipping the toggle in tenant settings.
Vulnerability disclosure, scope, what's already hardened, breach SLA, data residency, recovery objectives.
What we collect, where data is stored, our subprocessors, your rights.
The agreement governing use of the platform.
Need help?
Email hello@forigi.com with the URL of the page you're stuck on, your tenant name, and what you tried. We answer every message.