Publish a page to a private, Optimizely-only URL like /your-name/your-project/ — and it arrives with a backend already attached: a database, file uploads, AI, API calls that keep your keys off the page, and automations. It's behind Optimizely sign-in by default and shared with everyone or just the people you name. Five ways to deploy, plus everything you can do once it's live.
What it's for: design mockups, AI-generated demos, prototypes, internal microsites, dashboards — anything that builds to HTML/CSS/JS. Not for: server-rendered apps (Next.js SSR, Express…), public content, or anything over 250 MB. Every link requires Microsoft sign-in — nothing is ever public.
It's built specifically for this portal, so everything in this guide applies to what it makes: the same database, file uploads, AI and API connections. Apps from it carry a Site Builder badge, and any project you already have can be reopened there with Builder on its card. Full Site Builder guide →
index.html, a folder (dist/, out/, build/…), or a .zip.New here? Click “Deploy a sample app” on the empty dashboard for a live URL in two clicks. Just one HTML file? Drag the index.html straight on — no folder or zip needed.
A document, not a site
Drag a .md on with no index.html beside it and the portal renders it into a themed page — headings, tables, checklists, code — with a link that previews properly when you paste it into Teams. Drop several and you get one page with a nav across them, README.md first. Your .md files are kept exactly as you uploaded them; the page is added beside them, never in place of them.
This is the quickest way to share something an AI wrote for you. Ask it for the markdown rather than a web page, then drag the file on.
Which folder do I upload?
| Vite (React/Vue/Svelte/Solid) | dist/ |
| Create React App | build/ |
| Next.js (static export) | out/ |
| Astro | dist/ |
| SvelteKit (adapter-static) | build/ |
| Angular | dist/<app>/browser/ |
| Gatsby | public/ |
| Plain HTML/CSS/JS | the folder with index.html |
Rule of thumb: upload the folder that has index.html at its root.
The Optimizely Deploy connector is already set up org-wide. In Claude: Settings → Connectors → find Optimizely Deploy → Connect → sign in (stays connected 30 days). Then just ask:
Claude shows a preview and asks before deploying a real project. Big or multi-file build? If Claude can't upload (some chat sandboxes block it), save the files and drag them onto the dashboard — that always works.
Opalis the same server, already installed on Optimizely's instance — just ask it. It can be added to any Opal instance too; it signs each person in separately, so what it does is done as you. Opal has no outbound network of its own, so uploads travel in the conversation and cap out around 200 KB compressed — use the dashboard for a built app.
Using Claude Code (CLI), or adding it to your own Opal? Register once:
claude mcp add --scope user --transport http \ opti-deploy https://deploy.optimizely.com/api/mcp
That is all most people need: connecting also hands Claude the deploy playbook. Want it to apply without the connector, or pinned in your repo? The same instructions are published as a Skill at deploy.optimizely.com/skill — read it, or save it to .claude/skills/opti-deploy/SKILL.md.
Click Deploy from GitHub on the dashboard (top right) or any deployment card. The dialog walks you through it and generates a ready workflow filled in with your name, branch, project folder, and build command:
OPTI_DEPLOY_TOKEN (GitHub → Settings → Secrets and variables → Actions)..github/workflows/deploy.yml.name: Deploy
on:
push: { branches: [main] }
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci && npm run build
- name: Deploy to Optimizely Deploy Portal
run: |
cd "dist"
zip -qrX /tmp/deploy.zip . -x '.git/*' '.github/*' 'node_modules/*'
curl --fail-with-body -sS -X POST "https://deploy.optimizely.com/api/deployments/cli" \
-H "Authorization: Bearer ${{ secrets.OPTI_DEPLOY_TOKEN }}" \
-F "name=my-app" -F "mode=zip" -F "files=@/tmp/deploy.zip"The workflow is self-contained — paste it into any repo, nothing else to set up. Monorepo? Set the Project folder (e.g. next-app) in the dialog and the build runs there. Use folders relative to the repo — no leading slash.
Any script can deploy — no Claude, no GitHub. Create a token under Settings → Access tokens, then post a built folder to the deploy endpoint. Re-deploys keep the same URL and bump the version, exactly like every other route.
Using Claude Codein a terminal? That's the connector, not this — add it once with claude mcp add; the command is on above. This card is for when there's no agent involved at all.
A card carries the five things you do at a glance. Click it and you get the project page — everything else about that project, one click away instead of crowded onto every card in the grid.
On the card
Building it in Site Builder? It can publish a draft first — the same page served from here at /your-name/project/draft/, with your real database, your real sign-in and your real API connections, so you can try it properly before anyone sees it. A draft is visible to you and your maintainers only, even on a project the whole company can open, and it never counts against your project limit. Publishing it makes it the next version — and keeps the draft, so your next edit starts from what is live.
A project can exist as nothing but a draft — Site Builder claims the address when you first preview it, before anything is published. Those appear under Drafts on your dashboard rather than among your live sites, because nothing is served at their address yet. They cost you nothing: an unpublished project never counts against your limit.
On the project page
🔒 Only me — for while you're still building
One click hides a project from everyone but you. It doesn't overwrite your sharing — it suspends it — so unlocking gives you back exactly the sharing you had. It also outranks everything else: a locked page is hidden from the people and groups you shared it with, and so is the data it collected. Deploy straight into it by picking Only me at upload time.
👥 Groups — share with a group, not a list
Naming everyone works until it doesn't: you name them again on the next project, and nobody remembers to add the new joiner to all of them — which is why so much ends up public. A group is a named list of people you keep in one place, at Your groups. Share a project with the group instead of the people, then add somebody to the group once and they can open every project it's on — including ones you shared before they joined.
Groups are yours: you pick them in your own projects' Privacy dialog, and only you decide who is in them. People are emailed when you add them to a group, and when a project is shared with one — once, together, not one message each. Removing somebody, or deleting the group, takes their access away everywhere it applied, immediately.
🔗 A shorter link
Claim one on the project page and your site also answers at deploy.optimizely.com/go/your-name — no redirect, that URL just isthe site. It's an alias, never a rename: the original link keeps working forever, and the short one survives re-deploys and renames. First come, first served, and words that could look official (payroll, hr, sso…) are off-limits.
👥 Let someone else keep it updated
On the project page, under Editor access, add a colleague. They can then deploy new versions at the same link — nothing to re-share — and manage its data and sharing, from their own account. It shows up for them under Sites you maintain, and every version records who deployed it. They can'trename it, delete it, move its short link, or add more editors — that stays with you. Ideal for covering a page while you're on leave: the link never changes, so there's nothing to migrate when you're back.
Re-deploying under the same namebumps the version and keeps the URL identical — old links never break. That's the whole update story: there is no second URL to hand round.
Click My page in the top nav to go to /your-name — private to you:
One ★, two meanings
On your own page the ★ pins it; on someone else's it saves it to your Starred.
Someone shared a page with you?
Sharing is just sending the URL. Open it → it lands in Recently viewed automatically. ★ Save (the pill bottom-left of the page, or the star on any card) → it shows under Starred. You can also ask Claude: “star the page Denys sent me: <url>”.
Everything above belongs to you. A workspace is the answer when it should belong to a team: it is a namespace with people in it, and it owns the projects created in it. When somebody changes team or leaves, their work stays exactly where it is — nobody has to hand anything over, because it was never theirs to hand over.
Somebody outside Optimizely
Everything above shares with colleagues — “public” here has always meant everyone at Optimizely, never the internet. For a customer, an agency or a candidate, open a project and use External link on Project details: it mints one address that opens the project with no sign-in at all.
Making one
Workspaces in the top nav → New workspace. Anyone can make one — there is no approval to wait for. The address is permanent and comes from the same pool as everybody's username, so /design can be a person or a workspace but never both. You may own up to three; hand one on and you get the slot back.
Who can do what
Adding somebody emails them — it opens every project the workspace owns.
Putting work in one
Open the workspace → the Projects tab has the same uploader as your dashboard. What you deploy there lives at /{workspace}/{project}/ and belongs to the workspace: its data, API connections, AI budget and cap are all the workspace's. Site Builder can create there too — pick the workspace instead of yourself. An existing personal project cannot be MOVED in— a project is the workspace's from birth, and its data is stored under that name from the first write — but Duplicate copies one into a workspace you belong to, starting it fresh at version 1. The original stays where it is, and its data stays with it.
Winding one down
A workspace holding projects is never deleted — the links would break and the work would have no owner. An admin suspends it instead: nothing new goes in, everybody keeps what they can already open, and it can be reactivated at any time.
Your site is static, but it can still store and read data — surveys, feedback, sign-ups, polls, a shared list. No server, no database to set up, and no API key to paste anywhere. Easiest way: ask Claude.
Or write it yourself — this is the whole thing
One script tag, then name a collection on a form. That page is complete: it saves every submission and lists them back.
<script src="/collect.js"></script>
<form data-opti-collect="guestbook">
<input name="message" placeholder="Say something…">
<button>Sign</button>
</form>
<ul id="entries"></ul>
<script>
// The async wrapper matters: a plain <script> is a *classic* script, and
// top-level `await` there is a SyntaxError that kills the whole block.
(async () => {
const list = document.getElementById('entries')
async function render() {
const rows = await Opti.collection('guestbook').list()
list.innerHTML = rows
.map(r => `<li><b>${r.createdByName}</b> ${r.data.message}</li>`)
.join('')
}
document.addEventListener('opti:saved', render) // refresh after a submit
render()
})()
</script>You never asked who the visitor is — createdByNameis filled in for you. The page is served behind Optimizely sign-in, so the portal already knows, and the call carries that identity automatically. That is also why there is no key: a static site can't keep a secret, so we didn't give it one to keep.
Ask the collection a question
You don't have to pull everything down and sift it in the browser. Ask, and the server answers across every page — so the count you get back is about the collection, not about however much you happened to download:
const open = await Opti.collection('tickets')
.list({ where: { path: 'status', op: 'eq', value: 'open' } })
// Need the count as well as the rows?
const { records, total } = await Opti.collection('tickets')
.page({ where: { path: 'amount', op: 'gt', value: 500 } })Operators: eq, ne, gt, gte, lt, lte, contains, empty, notEmpty — the same set the automations use, so there is only one thing to learn. total is how many matched.
The same question is on the Data tab, with no code: set Show → entries where…, and once it has answered you can change or delete every entry that matched in one go — the middle ground between editing one row and emptying the whole collection.
Know the visitor up front
You don't even need to write a record to know who's there. Opti.me() hands you the signed-in person synchronously — greet them, or pre-fill a form, with no login and nothing to ask:
const me = Opti.me() // → { name, email } · null off-portal
if (me) greeting.textContent = 'Hi, ' + me.nameIt's the viewer's ownidentity, for display — the authoritative stamp on what they write is still set server-side, so it can't be faked from the page.
⚠️ If your page shows the data back
Reading defaults to you only. That's right for a survey — share it with the company, keep the answers to yourself — but it means a page that displays what it collected (a guestbook, a poll with results, a leaderboard) will look empty to your colleagues until you say otherwise. Open the project's Data tab and set Read: anyone who can open the app (or each person sees their own). The app can't widen that itself — only you can.
Saving from your own code
await Opti.collection('feedback').add({ rating: 5, note: 'love it' })
const rows = await Opti.collection('feedback').list()
// …and a second app of yours can read the first one's data —
// a public survey + a private dashboard over it:
const answers = await Opti.collection('responses', { app: 'survey' }).list()Attachments
A <input type="file"> inside a collecting form uploads itself — no extra code. From your own JavaScript, upload first and store what you get back:
const ref = await Opti.collection('applications').upload(input.files[0])
await Opti.collection('applications').add({ name: 'Ada', cv: ref })Up to 25 MB per file. They appear as downloads in your Data tab, and only people who can already read that entry can open them.
Calling another API (without a key in your page)
A page can't keep a secret — everyone who opens it can read the source. So save the credential once in your project's API connections panel and call it by name:
const me = await Opti.call('jira', { path: '/myself' })The key stays on the server and never reaches the browser. You choose the base URL and which methods are allowed — read-only by default — so a page can only do what you permitted. A connection belongs to you rather than to one project, so any page you deploy can name it, and anyone who can open those pages can use it.
Telling the other system who is calling. The credential is yours, so every call looks the same to the other end whoever opened the page. Turn on Caller identity on the connection and each call also carries a short-lived signed token naming the signed-in person — so a backend can look up their data. It comes from the session on the server, not from the page, which is what makes it worth trusting: a visitor cannot edit it or claim to be someone else. Off by default. Ask OptiXfor “caller identity” and it will give your backend team the three lines that verify it.
What you can store. One entry's data is up to 1 MB, and a named document — the keyed kind you'd cache an API reply in — up to 1.5 MB. Bigger than that wants a file, not a field.
The limits. A reply can be up to 20 MB, the body you send up to 8 MB, and a call has 180 secondsto finish. Go over and it fails with a message naming both the limit and the actual size — worth reading, because by then the other API has usually already answered, so it's the portal refusing to pass the reply on rather than your API failing.
Size is measured afterdecompression, so gzipping a big reply doesn't get it through. If you're near the limit, ask the other system for less — filter, paginate, or request fewer fields. Your page has to parse whatever comes back, so that's the faster page anyway.
Asking AI, without a key
Your page can summarise, classify, or extract — the portal holds the credential, so nothing secret goes in the page:
const themes = await Opti.ai('Group these comments into 3-5 themes', { input: text })It can also look things up and do real arithmetic. The plain helper searches the web and runs code when a question needs it, soOpti.ai('What changed in the news about X this week?') andOpti.ai('What is the median of these numbers?', { input: csv }) both work with no setup. Nothing to switch on — it is the same call.
⚠️ Adding up what you collected
input hands the model text. That is right for “group these comments” and wrong for money: a model totalling a column by reading it gets close and confidently misses — measured here at 0.4% out on a revenue column, the size of error nobody catches. Name the collection instead and ask for compute, and the rows go to a sandbox that runs real arithmetic:
// Exact — the rows are computed, not read
const total = await Opti.ai('What was the total revenue, by region?',
{ data: { collection: 'orders', compute: true } })
// No arithmetic needed, so no sandbox — faster and cheaper
const mood = await Opti.ai('What are people unhappy about?',
{ data: { collection: 'feedback' } })compute is your call, with no default — a spreadsheet declares itself tabular and rows cannot, so guessing would trade either the arithmetic or several seconds and real money on a question that never needed it. You can narrow first with where, using the same operators as above. It reads with your own access: nothing becomes visible that wasn't already.
Assistants — give it your handbook, and every page can answer from it
When a page needs the model to know something specific — a handbook, a price list, last quarter's numbers — write an assistant in Settings → Assistants: a short brief, plus files. Then tick it on for the projects that should use it (the project's AI tab), exactly as you would an API connection, and name it:
Each saved connection has a Try it button. Give it a path and it sends a real GET with your credential and tells you what came back — the status and how long it took. The credential is never shown again once saved, so this is the only way to catch a typo without deploying a page first. It only ever sends GET: on a write-only connection there is no safe test, and the button says so rather than filing something real on the other side.
const answer = await Opti.ai('What does the handbook say about expenses?', {
assistant: 'handbook',
})A project can have several ticked on, and one assistant can be on several projects — write it once, change it in one place. A call with no assistant is unchanged and still the common case.
Ask about a file
Send it with the question. One call, and the file is kept nowhere — no record, no collection, no copy:
const answer = await Opti.ai('What was the total, and what was it for?', {
file: input.files[0],
})Text and documents, data (.csv, .xlsx — loaded into a sandbox and computed with real arithmetic rather than read and estimated) and images, which the model looks at. Audio and video are refused before upload rather than after.
If you want to keep the file too, store it and name it by reference — but then the record is not optional, because it is what says whose file it is:
const ref = await Opti.collection('claims').upload(input.files[0])
await Opti.collection('claims').add({ who: 'Ada', receipt: ref })
await Opti.ai('What was the total?', { file: { collection: 'claims', id: ref.id } })Add files and we sort them out. Prose — handbooks, notes, PDFs — is searched and quoted back. Spreadsheets and CSVs are loaded into a sandbox and computed, which is the part that matters: a model asked to total a column without running code adds it up in its head, confidently and wrongly. Each file shows which way it went, and adding a spreadsheet turns the code tool on for you.
⚠️ The audience of a document is every project the assistant is on. Anyone who can open one of them can ask about it, and the answer quotes it — so ticking an assistant onto a project shared with everyone at Optimizely publishes its documents to everyone at Optimizely. This is the one way an assistant differs from a connection, whose key never leaves the server. Settings shows which projects each one is on; that list is the audience.
Unlike everything else here, this costs money each time— so it's off until you turn it on for a project (AI on the project page), and every call spends yourmonthly allowance even when a colleague is the one who clicked. Put it behind a button, and send many items in one call rather than one call each. The same panel shows what you've used this month, in credits: you get 200 a month, and 1 credit = 100,000 tokens. A typical call is a small fraction of one, so most months read under 1 credit — the meter is for the page that gets away from you. One call takes up to 800,000 characters in and answers with about 16,000 tokens (raise it with maxOutputTokens, up to 128,000) — enough for a whole collection in one call, so summarising in batches is rarely the right instinct now.
Something on a timer
Your page only runs while someone has it open, so anything recurring is set up in the portal rather than in code. The Automations tab is where you say what should happen without you — start from a recipe and edit what it fills in. An automation runs either on a clock (hourly, daily or weekly — times in UTC) or whenever an entry arrives, which fires on each submission in seconds for the things that cannot wait. And it does one of these:
Opti.collection('x').doc('key').get(). This one needs an API connection rather than a channel, and it is always read-only — a scheduled job never writes to the other system.What you get
In a hurry? Open any project and hit Connect → Database. Tell it what you're doing and it writes the snippet for you — ready to paste.
Sometimes there is no page in the story: you want somewhere for another system to send data, a query that runs every morning, or a weekly summary posted to a channel. On the dashboard, under the upload bar — “No page? Collect data instead.” It asks for a name and nothing else.
It opens on its Automations tab, because that is the only tab with anything to do on a brand-new one. Start by naming a collection — one kind of thing this project stores, like tickets or signups — and everything below writes into it. You can add more collections later from the same place, or from the Data tab.
Browse is in the top nav for everyone. It shows every public project in the org — no opt-in, nothing to tick — plus any private ones shared with you. Starter apps stay out of it, and a locked project is hidden from everyone.
It opens on colleagues' work, because your own already has a home on your dashboard. Sort by most opened this month (counted in people, not clicks), recently updated, or newest. Search covers names, notes and tags.
Tags are how a project gets filed — add up to 20on the project page or straight on its card, and click one anywhere to see everything else like it. Your own projects there keep the full owner toolset; colleagues' are read-only with a ★ Save.
Bottom-right of every page. It knows this portal, sees the same things you can, and answers in a sentence instead of sending you here to read.
Worth asking: “who can open this, and why can't Sam?” · “what has my form collected?” · “how do I share with a whole team?” · “is anything waiting on me?” · “has anyone built this already?”
Every lookup runs as you. Your projects in full; colleagues' only if they're public or shared with you — and then without their allowlist, because who else can open someone's project is theirs to know. It cannot read a private project you can't open, or a credential, ever.
Ask it to make a group, file a request, change privacy, or attach a saved group / connection / channel, and you get a confirmation card: what happens, who it reaches, how many get emailed. Nothing until you press it — and it goes out as you, so the audit trail has your name on it.
It can't deploy, rename or delete a project, add editors, or create a connection or channel — those hold a secret, and a secret is typed into Settings by a person. Unlike Opti.ai() on a page, OptiX has no monthly budget— ask as much as you like. Conversations are saved so you can come back to them; an admin can read one, and it's recorded when they do.
Still stuck? Use the support button on a deployment card, or .