Self-hosting
Everything here is optional. Run EDI on your own machine and the local SQLite store is already doing the job. This page is for the other case: putting it on a server, where the storage changes, the data leaves your machine, and a public URL can spend your money.
Storage
A SQLite file, holding workspaces and chats. It writes to EDI_DATA_DIR, ./.edi-data/ by default. There is nothing to provision, no migration to run and no second set of credentials to keep out of the browser bundle.
/tmp, which does not survive between invocations, and two consecutive requests are not guaranteed to reach the same instance. Whichever one handled your upload is rarely the one handling your next question, so there is nowhere for a workspace to live. A VPS, a container with a volume, or any host that gives you persistent storage is fine.If a file is the wrong shape for where you are putting this, the seam is in backend/stores/: a second store is one module and one import rather than a hunt through the app.
Deploying
Two processes, a Python ASGI app and a Next.js app, so host them the way you host those. Nothing in this project is written for a particular platform. What it needs from wherever you put it is three things:
- A disk. Somewhere writable that survives a restart, for the SQLite file the workspaces live in.
- A route from the browser to the API. Simplest is one origin: a reverse proxy sending
/api/*to the Python process and everything else to Next. Then CORS never enters into it. - A model: a provider key, or an Ollama the backend can reach.
uvicorn main:app --host 127.0.0.1 --port 8000 --app-dir backend
cd edi-frontend && npm run build && npm startSeparate domains
If the frontend and the API are not on one origin, name the origins the browser will be on:
EDI_CORS_ORIGINS=https://edi.example.com,https://staging.example.comA wildcard is refused rather than accepted with a warning. It used to be the default here, and what it meant in practice was that any page on the internet could call this API from a visitor's browser, including the endpoints that spend model calls.
Deploying the documentation without the app
This site is that deployment. The app is at /app, and it is there in every clone; what the hosted copy does is redirect it away, because an empty spreadsheet asking a stranger for a file they have not got is a worse landing page than the documentation. One variable at build time:
EDI_DOCS_ONLY=1Read by edi-frontend/next.config.ts, not the backend, so it belongs in the frontend's environment. Unset, which is the default and what you get by cloning, /app serves the whole application. It is not inferred from anything about the host, because a deployment that looks from the outside like somewhere you would put documentation may be exactly where you want the whole app.
Putting it on a public URL
Put authentication in front of it. A reverse proxy asking for a password is the whole answer, and it is a better one than any setting this project could offer.
There are no usage caps. There used to be, sized for a public demo that no longer exists, and every one of them was off unless you switched it on. On your own machine they were an obstacle and nothing else: your model, your key, your bill.
Which leaves the shape of a public deployment plain, and worth stating rather than mitigating badly. There is no sign-up, so every visitor is anonymous. Every question is a model call charged to you, and nothing counts them. Anyone who knows a workspace UUID can open it. And the model picker will let a visitor repoint the backend or store a key on your disk unless you turn it off:
EDI_ALLOW_MODEL_SWITCHING=0That last one is worth setting even behind a password, because it is the only one where a visitor's action lands on your filesystem.
How big a sheet it will open
Eleven megabytes of data, and a bigger sheet is refused with a message saying what would fit. That is about 100,000 rows six columns wide, 50,000 at twelve columns, 13,000 at forty.
Of data, not of file. Nothing caps the file you upload. What is measured is the sheet after parsing, as the JSON the browser is handed, which is a different number in both directions:
| The same 100,000 x 6 sheet | Size | Capped? |
|---|---|---|
| as a CSV on disk | 3.18 MB | no |
as an .xlsx on disk | 2.57 MB | no |
| as rows in the browser | 10.05 MB | yes, against 11 MB |
An .xlsx is a zip, so a 2.5 MB upload can be 10 MB of sheet. A workbook can also go the other way: EDI reads the first tab, so a 5 MB file of twenty tabs and a chart may be a tenth of a megabyte of data. A limit on bytes would be wrong in both cases, which is why there is not one.
Where the number comes from
The ceiling is the browser, not the server. The grid holds a JavaScript object per cell and runs out of memory; the backend parsed and stored 200,000 rows in three seconds. It is a cliff rather than a slope, so past it you get a spinner that never stops rather than a slow sheet, which is why it refuses instead of trying.
Fifteen sheets were measured, one fresh browser each, timing an upload through to a grid that had painted and could still answer a frame. Neither rows nor cells predict where it falls over:
| Sheet | Cells | CSV | JSON | Result |
|---|---|---|---|---|
100,000 x 6 | 600,000 | 3.2 MB | 10.0 MB | 7.8s |
110,000 x 6 | 660,000 | 3.5 MB | 11.1 MB | 8.0s |
120,000 x 6 | 720,000 | 3.8 MB | 12.1 MB | never finished |
55,000 x 12 | 660,000 | 4.3 MB | 12.2 MB | never finished |
27,500 x 24 | 660,000 | 4.8 MB | 13.1 MB | never finished |
100,000 x 12 | 1,200,000 | 7.8 MB | 22.2 MB | never finished |
660,000 cells renders six columns wide and hangs at twelve, so cells are not it. 110,000 rows renders and 55,000 hangs, so rows are not it either. The JSON column is the only one with every success above every failure, and the cap sits at 11 MB: a tenth below the smallest sheet measured to hang.
EDI_MAX_DATA_MB moves it, and EDI_MAX_DATA_MB=0 removes it. These numbers came off one machine, and one with less memory meets the wall sooner.
Worth knowing
- Nothing caps the size of an upload, and the backend reads and parses the whole file before it can measure anything. Refusing a 30 MB file takes about three seconds; a file large enough to exhaust memory will do that first.
- Anyone who knows a workspace UUID can open it. They are unguessable, but this is not access control. Do not put anything sensitive in a deployment you have shared.
- The
@univerjs/*packages are pinned to one version by anoverridesblock inedi-frontend/package.json. Upgrading one without the rest breaks the spreadsheet.
How it works explains why the backend is stateless and what that costs.