Server Setup
Indx is a self-hosted server that wraps the Indx search engine and exposes it over HTTP. It has three interfaces:
- Web UI — a Blazor dashboard where you can register users, upload and configure datasets, preview search results, manage teams, and check engine health. No code required.
- REST API — the full HTTP API, used from any language or framework. All operations available in the UI are also available programmatically.
- Monitor — a live view of the running instance: what each dataset is doing, what the process is using, and what just happened. In the console under Admin → Monitor, and in the terminal you started the server from. See Monitor.
Swagger UI is available at /swagger for interactive API exploration.
Quick Start
git clone https://github.com/indxSearch/Indx
cd Indx
dotnet runRequires .NET 10 SDK. The server starts at https://localhost:5001.
Register a User
- Open
https://localhost:5001/Account/Registerin a browser - Enter your email and password
- Submit — you're now logged in as the first user
By default, registration is open. See Registration Control below to restrict who can sign up before deploying to production.
Get an API Key
To call the REST API you need an API key.
- Log in at
https://localhost:5001/Account/Login - Go to Account → API keys (
https://localhost:5001/account/api-key) and click New API key - Choose the team, the access level and, optionally, the datasets the key may reach:
- Search only — for websites and apps. The key is visible in the browser, and this is all it can do.
- Read only — for exports, reporting and agents. Keep it on a server.
- Full access — for your own servers and pipelines: everything your team role allows.
- Choose how long it lasts (30, 90, 180 or 365 days), click Create key and copy it — it is shown only once (Search only and Read only keys can be shown again from the list)
A key never exceeds your role in the team, and cannot be changed later: create a new one and revoke the old. See Authentication for what each level can call.
For a production website or a shared server, use a team key. A team Admin creates it under Manage team → API keys. It belongs to the team, so it keeps working when the person who created it leaves, and every Admin of the team can see and revoke it. A team Search only key can be set to never expire. See Personal keys and team keys.
Send the key as a Bearer token on every request:
curl -H "Authorization: Bearer <token>" https://localhost:5001/api/...What You Can Do in the UI
| Feature | Description |
|---|---|
Upload data | Stream a JSON file into a dataset |
Configure fields | Set searchable, filterable, facetable, sortable, and weight for each field |
Index | Trigger an index build and monitor progress |
Categories | A catalogue's hierarchy as a tree: every category with its product count, the chosen one with its products, a step to boost it. Read from a path field such as product_type or from fields that each hold a level; see the how-to |
Search preview | Run searches directly in the browser and inspect results |
Statistics | Searches, clicks and conversions for the last 7, 30 or 90 days: when people search, where from, which result they choose, which languages, how they type, and a profile of the window against the one before; top queries, fuzzy finds, top documents, browsing and the dataset's own changes |
Health & status | See engine state, document count, index time, and whether a shadow build is in progress |
Teams | Manage team membership and roles (Admin / Editor / Viewer) |
Transfer dataset | Move a dataset to another team |
Teams
Indx organises access around teams. A team owns datasets, and every member of the team has the same role on all of the team's datasets. A user can belong to multiple teams, with a different role on each. Membership is managed from the account portal's team pages.
Roles
| Role | Can search | Can modify data & fields | Can delete / transfer |
|---|---|---|---|
Admin | ✓ | ✓ | ✓ |
Editor | ✓ | ✓ | — |
Viewer | ✓ | — | — |
Inviting people
Invite a colleague straight into a team: in the team's settings, choose Invite member, enter their email address and pick a role.
- If they already have an account on the server, they are added to the team at once.
- If they don't, they get an email with a link to register. When they register, they join the team in that role, and no team of their own is made for them. They can create one later with New team.
An invitation lasts 14 days. Pending invitations are listed in the team's settings, where you can send one again (which gives it another 14 days) or revoke it.
Team editors can add people who already have an account. Inviting someone new takes a team admin. A server admin can turn that off under Admin, Settings, Registration, and then only server admins can invite new people. Server admins can also invite from Admin, Users, to any team or to the server alone, in which case the new user gets a team of their own.
An invited address can register whatever the registration mode is, even when registration is closed. The mode decides who may register without an invitation.
Transferring a dataset between teams
An Admin of both the source and target team can move a dataset:
curl -X POST https://localhost:5001/api/teams/acme/datasets/products/transfer \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{ "targetTeamName": "research" }'Deploy to Azure App Service
- Create an App Service with Windows OS and .NET 10 runtime
- Publish with
dotnet publish -c Releaseand deploy - Set environment variables (use
__as the section separator):Jwt__Key— requiredRegistration__Mode— recommended:EmailDomainorClosed
- Configure CORS with your frontend origins
- Place the
.licensefile inD:\home\data\for unlimited documents
Production Checklist
- Change the JWT signing key
- Set registration mode
- Configure CORS
- Set up an email provider if you want email confirmation
- Place the license file
- Set up OAuth if needed
Monitor
A live view of the instance: what each dataset is doing right now, what the process is using, and what just happened. Two ways in, reading the same thing.
In the console, under Admin → Monitor: the event stream and the instance figures, the process's CPU and memory against what the machine allows, the native memory pool, searches a minute and every dataset's state, with no terminal needed. This is the one to use on a hosted deployment. It is admin only, because an event names the team whose dataset changed, and the events are held in memory, so they start again when the server restarts.
In the terminal, if you started the server from one, drawn there automatically.
The dataset table gives state, document count, records on disk, time since last use and the idle-eviction countdown, coloured by state to match the chips in the web UI. Below it an event stream carries dataset state changes, idle sweeps and background jobs as they occur.
Ctrl+Q detaches the monitor and leaves the server running. Ctrl+C stops the server as usual, and
F10 shuts it down after a confirmation. The divider between the two panes can be dragged, and
where you leave it is remembered.
Piping it somewhere else
Azure log stream, docker logs and the systemd journal are pipes rather than terminals, so they
cannot show a full-screen application. There the monitor prints a compact status block instead,
appended every 30 seconds, with events as they happen. It is off by default, since log volume is
usually billed:
{
"Indx": {
"Monitor": {
"Enabled": true,
"StatusSeconds": 30
}
}
}Set Indx__Monitor__Enabled to true in App Service application settings to get the same thing in
the portal's log stream.
To turn the monitor off everywhere, set Indx:Monitor:Enabled to false or start with
--no-monitor. A startup that fails never draws it, so an error such as a port already in use is
printed the way it always was.
Configuration Reference
All settings live in appsettings.json or as environment variables.
JWT Security
A strong signing key is generated on first run and stored in ./IndxData/jwt.key, so a single instance needs nothing. Set Jwt:Key explicitly (Key Vault, user secrets or the Jwt__Key environment variable) when several instances must accept each other's API keys:
dotnet user-secrets set "Jwt:Key" "your-secret-key-minimum-32-characters"Updates
Once a day the server asks license.indx.co which Indx release is the latest. When it is newer than
the one running, every admin gets one notification in the console, and the admin area shows the new version under the running one. An ordinary release is not emailed unless an admin turns that on; a security release is. The request is anonymous:
no token, no instance id. Switch it off with Updates__Check=false.
Rate limits
Three limits, all under RateLimits in appsettings.json or as environment variables (RateLimits__Api__RequestsPerSecond and so on). Every one answers 429 with code: rateLimited, retryAfterSeconds and a Retry-After header, so clients need a single handler. Set Enabled: false to switch one off.
| Section | Default | Protects | Settings |
|---|---|---|---|
Auth | on | Login and password forms from guessing, per client address | PermitLimit 10 attempts per WindowSeconds 60 |
Api | off | Other users of the instance from one runaway integration; one token bucket per API key | RequestsPerSecond 50, Burst 200 |
Anon | on | Scanners hammering the API without a valid token, per client address | PermitLimit 30 per WindowSeconds 60 |
{
"RateLimits": {
"Auth": { "Enabled": true, "PermitLimit": 10, "WindowSeconds": 60 },
"Api": { "Enabled": false, "RequestsPerSecond": 50, "Burst": 200 },
"Anon": { "Enabled": true, "PermitLimit": 30, "WindowSeconds": 60 }
}
}Sizing notes:
Apiis off on self-host because on your own hardware the ceiling is the hardware. Turn it on when several teams share an instance and one of them could monopolise it.Anoncovers requests whose bearer token is missing or does not validate, which all end in401anyway. Raise it only if a client of yours legitimately polls while unauthenticated; CORS preflight is already exempt.- Rejections are logged under
IndxServer.RateLimitand counted asindx.ratelimit.rejections, tagged by which limit fired. - There is no instance-wide cap on concurrent work. A dataset that is already rebuilding answers
409rather than queueing another rebuild behind it, and the engine bounds its own searches per dataset. - Behind a reverse proxy or Azure App Service, set
ASPNETCORE_FORWARDEDHEADERS_ENABLED=trueso the per-address limit sees the client's address and not the proxy's; otherwise every caller shares one window.
Registration Control
Controls who can create an account:
{
"Registration": {
"Mode": "EmailDomain",
"AllowedDomains": ["yourcompany.com"]
}
}| Mode | Behavior |
|---|---|
Open | Anyone can register (default) |
EmailDomain | Only emails from the listed domains |
Closed | No new registrations |
Email Provider
Indx uses Azure Communication Services for email (password reset, confirmation). If not configured, all emails are printed to the console instead — fine for development.
{
"AzureCommunicationServices": {
"ConnectionString": "endpoint=https://...",
"SenderAddress": "noreply@yourcompany.com"
}
}OAuth (Optional)
Google and Microsoft login are supported. Configure via user-secrets or environment variables:
{
"Authentication": {
"Google": {
"ClientId": "...",
"ClientSecret": "..."
},
"Microsoft": {
"ClientId": "...",
"ClientSecret": "..."
}
}
}Database
Three SQLite databases are created automatically on first run — no setup required:
identity.db— user accounts and authenticationindx.db— your documents, field configurations, synonyms and boost rulesstats.db— search statistics, clicks and conversions (Statistics:DbFilemoves it)
The search index itself lives in memory. Documents are kept in indx.db, so after a restart the
server rebuilds every dataset's index on its own, in the background; a search that arrives first
loads its dataset on demand. Nothing has to be loaded again by hand.
License
Place the .license file in ./IndxData/ (local) or D:\home\data\ (Azure) to remove the 100,000 document limit. See Licensing.