Attach download links to a whole file list, not just one record
?presign=true on the files collection endpoint mints a working downloadUrl on every item in the page, opt-in so a plain list call never pays for links nobody asked for.
Changelog
A quarter of shipping — Jun 22 – Sep 20, 2026. Compiled from engine, MCP, and indexer release history. Newest first.
Quarter at a glance
Bulk create, read, update, delete and upsert
Send a JSON array to any collection endpoint for bulk write (up to 100 items), or ?id=a,b,c for bulk read. Runs on its own Lambda so a large batch job can't starve regular traffic; each item gets its own success/failure result.
Custom Endpoints
Attach a Function to a URL beyond ordinary CRUD, e.g. POST /orders/summary. Supports public (unauthenticated) routes, synchronous or asynchronous execution, and optional request validation for body and query parameters.
Refresh tokens, and a 1-hour access token to go with them
Access tokens shrank from an unrevocable 7 days to 1 hour. Session longevity now lives in a revocable refresh token (POST /auth/refresh) — a leaked access token expires within the hour instead of staying valid for a week, and a deactivated account loses access within the hour too.
Aaly Functions — customer-authored server-side code
Attach your own Python to an entity's request lifecycle: before or after create/read/update/delete, synchronous or asynchronous, chained in order. One primitive covers validation gates, computed values, webhooks and third-party integrations — no separate feature for each.
File storage — presigned uploads, a files entity, and downloads
Request a presigned URL, upload directly to S3, confirm — then attach files to any entity via an ordinary reference field, with reverse lookup and metadata embedding for free. Downloads are short-lived presigned URLs minted fresh on every read. files is extensible with your own custom fields, same as users.
Major query performance improvements
A bounded date-range filter (e.g. "last 7 days") previously scanned up to 36 months of index buckets; now collapsed into one lookup — roughly 3x faster in production. Paginated collection reads switched to direct batch key lookups instead of a slower partition scan — roughly 2.7x faster. Measured on live traffic: orders GET p50 dropped from ~740ms to under 250ms.
Zero-config MCP connect via OAuth 2.1
claude mcp add --transport http aaly https://mcp.aaly.io/ now works with no manual API key. Sign in through a browser and Claude Code or Cursor connect automatically — self-hosted authorization server with PKCE and Dynamic Client Registration.
Search & filtering, redesigned
Reference fields are now embedded directly on the parent record instead of stored as separate child records. A new asynchronous index table (fed by DynamoDB Streams) backs full query-operator filtering — eq, gt/gte/lt/lte, in/nin, starts — at scale, replacing the old reference-only query path.
?presign=true on the files collection endpoint mints a working downloadUrl on every item in the page, opt-in so a plain list call never pays for links nobody asked for.
Send a JSON array to any collection endpoint for bulk write (up to 100 items), or ?id=a,b,c for bulk read. Runs on its own Lambda so a large batch job can't starve regular traffic; each item gets its own success/failure result.
Previously returned a raw 500.
Stopped leaking raw exception text from authentication failures; also tightened a Custom Endpoint's execution budget to line up with API Gateway's own hard timeout.
$increment / $append on PATCHPATCH {"stock": {"$increment": -3}} applies directly at the database layer, in one write — no more lost-update races on counters like inventory. min/max bounds are enforced as part of the same write and a violation returns a normal 400 with the value it would have produced.
users and filesA Function or Custom Endpoint attached to either of these built-in entities was silently never invoked. Both now behave like any customer-created entity.
runAs: "system" privilege escalationIt granted no real authority (no RBAC exists yet to use it) and had a chain-wide footgun — one Function requesting it silently affected every other Function in the same request chain, including ones that never asked.
Declare a field as round(quantity * unitPrice, 2) or concat(project, '-', name) and it's computed automatically from other fields on the same record, sandboxed, no custom code required. Also generalized automatic unique-key enforcement for entities and fields to be scoped per project/entity instead of per customer.
A rapid burst of writes to records sharing one filterable value could cause the indexer to drop one record from filtered/faceted results permanently, with the underlying data completely intact. Error responses now also carry a requestId for support correlation.
New GET /requests and GET /function_logs — every API call and every Function invocation is now queryable: status, latency, error text, and captured console output. Answers "was that a bug in my code or the platform's."
Attach a Function to a URL beyond ordinary CRUD, e.g. POST /orders/summary. Supports public (unauthenticated) routes, synchronous or asynchronous execution, and optional request validation for body and query parameters.
platform_variables/platform_secrets set a project's default; variables/secrets override it for one tenant. A Function always sees the merged view, resolved automatically.
Every request and every Function invocation it triggers now shares one requestId, so a full call trace can be reconstructed end to end.
Access tokens shrank from an unrevocable 7 days to 1 hour. Session longevity now lives in a revocable refresh token (POST /auth/refresh) — a leaked access token expires within the hour instead of staying valid for a week, and a deactivated account loses access within the hour too.
The single platform-capabilities doc served over MCP became per-feature guides — data modeling, querying, auth, files, functions, REST conventions — each with worked request/response examples.
variables and secrets split into distinct entitiesReadable config and encrypted credentials now have contracts suited to each — secrets are never returned by the API and are hard-deleted on revocation, instead of both being one entity behind a boolean flag.
A PATCH that rotated a stored secret (an API key, an OAuth token) was writing the new value to the database unencrypted. Rotation now goes through the same encryption path as creation, and fails closed if it can't verify that.
Attach your own Python to an entity's request lifecycle: before or after create/read/update/delete, synchronous or asynchronous, chained in order. One primitive covers validation gates, computed values, webhooks and third-party integrations — no separate feature for each.
upload_file toolUpload a file to the platform project's own files entity directly from an MCP session — by inline bytes, or by minting a presigned URL for a sandboxed agent to POST to directly.
Bucket provisioning failed for any customer or project slug containing an underscore; separately, presigned URLs always resolved to the global us-east-1 endpoint instead of the bucket's real region. Both fixed after being caught in live testing.
files entity, and downloadsRequest a presigned URL, upload directly to S3, confirm — then attach files to any entity via an ordinary reference field, with reverse lookup and metadata embedding for free. Downloads are short-lived presigned URLs minted fresh on every read. files is extensible with your own custom fields, same as users.
A bounded date-range filter (e.g. "last 7 days") previously scanned up to 36 months of index buckets; now collapsed into one lookup — roughly 3x faster in production. Paginated collection reads switched to direct batch key lookups instead of a slower partition scan — roughly 2.7x faster. Measured on live traffic: orders GET p50 dropped from ~740ms to under 250ms.
Previously required email specifically; now accepts either identifier, matching the flexibility ordinary user signup already had.
ChatGPT's connector rejected a spec-compliant null client_secret for our public, PKCE-only OAuth clients. Worked around with a real, unused placeholder secret purely for interoperability.
claude mcp add --transport http aaly https://mcp.aaly.io/ now works with no manual API key. Sign in through a browser and Claude Code or Cursor connect automatically — self-hosted authorization server with PKCE and Dynamic Client Registration.
Creating a tenant with only an email gives its first user a passwordless account that signs in via Google OAuth.
New projects.publicSignup flag (default off) gates whether anyone can self-register a brand-new tenant; tenants.publicSignup independently gates joining one existing tenant. A project owner can bootstrap their first tenant and user directly via the API even with public signup off.
role/type from the universal users schemaBoth were client-settable on every project by default with no gating, and would have collided with a future permissions model. Add your own role field explicitly if you need one.
GET /entities / GET /fields now include built-in system schemaPreviously only showed your own custom entities and fields. Now also lists entities, fields, tenants and their base fields, each tagged _system: true.
users becomes extensibleTenant records now live on the platform table and are managed platform-admin-only. users is now a hybrid entity — add your own custom fields via POST /fields exactly like any of your own entities, while base fields (email, password…) stay fixed.
Internal upgrade to the current protocol SDK; no change to tool behavior.
update_field silently dropping immutable attributesA PATCH that mixed immutable and mutable field attributes was quietly discarding the immutable ones.
?facets=Any searchable field on an eligible type (enum, boolean, list, reference, date) is now automatically faceted — distinct-value counts, no separate opt-in flag. ?facets=status,priority lets you fetch just the fields you need.
A record relying on a field's schema default (rather than an explicit value) returned "true"/"false" instead of a real boolean, silently breaking search:true filtering on that field.
Reference fields are now embedded directly on the parent record instead of stored as separate child records. A new asynchronous index table (fed by DynamoDB Streams) backs full query-operator filtering — eq, gt/gte/lt/lte, in/nin, starts — at scale, replacing the old reference-only query path.
Unique-value reservation records and the schema cache are now correctly reconciled on PATCH; empty PATCH transactions are guarded against.
Enabled by default at table creation for both platform and project DynamoDB tables.
A key minted with context: {"projectId": "PRJn"} pre-scopes MCP tool calls to that project, so an agent never has to pick or type a project id.
Two different organizations can now share a display name. Hyphens are disallowed in customer/project slugs, since they feed directly into DynamoDB table names.
multiTenant signup gating now enforced correctlyAlso: list fields now require a declared valueType (string/number/boolean/object).
Added multi-step recipe prompts and describe_project — a one-shot summary of a project's tenant model, entities, relationships and filterable fields.
Every request now produces a paired entry/exit log line with duration, status, and whether the caller was a browser, MCP, or another client — a debugging aid, not an access-control signal.
_ prefixid, status, createdAt and friends became _status, _createdAt, etc. You can now safely name your own fields status, type, or createdAt with zero collision risk.
https://mcp.aaly.io/ is now the endpoint directly — no /mcp path segment.
A token for one customer/project could previously be presented against another project's URL slugs.
aaly / platformThe reserved admin customer/project slugs moved from thunderbase/admin to aaly/platform.