# Bounded native app services — SDK 1.11.0 Hackemist remains a static browser-app platform. Rendering, document processing, conversion and game engines run on the user's device or an approved external backend. The native services below add small durable records and declared transactions. They do not host uploaded server programs, arbitrary SQL, cloud file drives, video processing or custom worker code. For cloud files, large datasets, custom engines and provider-specific services, publish the static frontend normally and connect it directly to your separately hosted HTTPS/WSS backend. Use Studio → App details → Browser compatibility → External backend, or `connections_request`. Exact origins retain administrator review and plan checks. Your backend owns its authentication, CORS, credentials, data and billing; Hackemist credentials are never backend credentials. Reviewed embedded frames remain a separate option, not an unrestricted website proxy. ## Declare and release Package `hackemist-services.json` beside `index.html`. Studio → App details → Native app services and MCP `native_services_update` stage the same versioned source file with a current `base_version_id`. `native_services_get` reports only declarations, limits and aggregate usage, never runtime user records. Existing `files:read`/`files:write` credentials do not become user-data credentials. Normal publication validates and activates the saved declaration atomically. No per-app administrator provisioning, connection string or service-role key is needed for eligible native modules. Site-wide enablement and current account/app access still apply. An app support manifest describes requirements; it does not grant permissions. Schema version 1 accepts only the closed fields documented here; unknown fields and executable definitions fail explicitly. The editor/discovery schema is served at `/docs/native-services.schema.json` and MCP resource `hackemist://schemas/native-services`; semantic validation also checks module dependencies, declared fields, size bounds and schema compatibility. ```json { "schema_version": 1, "modules": ["shared", "config"], "config": {"theme": "blue"}, "namespaces": { "work": { "version": 1, "collections": { "tasks": { "schema": { "type": "object", "properties": { "title": {"type": "string", "maxLength": 160}, "status": {"type": "string", "enum": ["open", "done"]} }, "required": ["title", "status"], "additionalProperties": false }, "read": "owner", "write": "owner", "indexes": ["status"], "search": ["title"] } } } } } ``` Modules are `shared`, `actions`, `links`, `notifications`, `jobs`, `config`. Actions/links/notifications require shared; jobs also require actions. There are at most four namespaces, sixteen collections and sixteen actions per app. Schemas use the existing bounded local save-schema subset (types, closed properties, required, scalar enums, items and size/numeric bounds); no schema references, remote lookups or executable migration code are accepted. ## Shared records ```javascript const shared = await Hackemist.shared.open({namespace: 'work', schemaVersion: 1}); const tasks = shared.collection('tasks'); const operationId = Hackemist.operationId(); // Keep with the exact payload. const task = await tasks.create({title: 'Prepare report', status: 'open'}, {operationId}); await tasks.update(task.id, {status: 'done'}, {expectedRevision: task.revision, operationId: Hackemist.operationId()}); const page = await tasks.query({filters: [['status', '==', 'done']], limit: 20}); const stop = tasks.watch(task.id, value => render(value), {onError: showError}); // Stop when the view closes; shared.dispose() stops all its watches. ``` `render` and `showError` belong to your app. Records expose `id`, `data`, opaque `owner`, `group`, `revision`, `archived`, `createdAt`, `updatedAt`. Identity and scope come from the trusted launch, never a browser-supplied account/role field. `shared.actor` is stable for account/project across devices and save-slot changes; it differs across unrelated apps. It is an identifier, not an access credential. Reads default to `owner`. Other read policies are `public`, `members`, `managers`. Writes default to `owner`; alternatives are `members` (editor/manager), `managers`, and `actions` (ordinary writes forbidden). Private group records additionally require current membership, including their creator after removal. App launch access remains an outer requirement: a public collection cannot bypass a private or Plus app. `managers` with no group means the app owner; declaring that policy is an explicit disclosure to the app manager. App ownership alone does not grant access to other owner-private or group-private records. `protected` lists fields requiring managed actions. `unique` lists up to three constraints, each containing one to three indexed scalar fields; uniqueness is within collection/resource group. Group/owner metadata cannot be patched through ordinary data updates. Archive retains data, counts toward quota and removes its active indexes. `delete(id,{expectedRevision,confirmed:true,operationId})` permanently removes an ordinary writable record and frees its record quota. Protected action records cannot be deleted through the ordinary record API. Archive/delete are explicit; stopping a module or unpublishing never automatically erases records. Indexed strings require a maximum of 256 characters, either explicitly through `maxLength` or through a bounded scalar enum. Native numeric values stay inside JavaScript's safe numeric range; encode larger identifiers/integers as strings. Queries support up to three indexed filters (`==`, `<`, `<=`, `>`, `>=`), one ordering field (`id`, `createdAt`, `updatedAt`, or an indexed string/number), cursor pagination and at most fifty records. Missing optional ordering values are excluded from indexed ordering. Search uses one to four whole-word tokens from up to two declared text fields; it is not fuzzy/global/vector search. Aggregates return permission-filtered counts and optional `sum` of a numeric index; selections over 500 records must be narrowed. No arbitrary joins or SQL. `watch` is bounded change polling, not a push socket: five seconds while visible and online, backoff up to sixty seconds after failure, sixteen watched records per handle and eight open handles. One bounded metadata query checks only the explicit watched IDs; unchanged records trigger no snapshot reads. Scope-specific version digests expose no other users' activity counts. Membership revocation marks the requested record unavailable. Slow/offline clients recover by rereading snapshots, without treating a notification as authoritative data. Previously delivered content cannot be recalled from a user's device. ## Resource membership and links ```javascript const group = await shared.groups.create('Project team'); const invitation = await Hackemist.links.create({namespace: 'work', group: group.id, role: 'editor', maxUses: 1}); // Display invitation.url. It is an ordinary /app/PROJECT?resource=ID link. // On the recipient's explicit Accept action: const {resource} = await Hackemist.links.context(); const details = await Hackemist.links.resolve(resource); await Hackemist.links.accept(details.id, {operationId: Hackemist.operationId()}); ``` Groups use viewer/editor/manager roles. Managers can change existing memberships with `setRole(group,actor,role,expectedRevision)`; the last manager cannot be removed without a replacement. New members enter through explicit invitations. Invitations grant only viewer/editor access, expire within thirty days, have bounded uses and can be revoked. Opening/resolving a link does not join or mutate. Expiry, active organizer role, app access and membership are checked server-side. Live room codes retain their temporary purpose. Native links expose no launch capability, credentials or global user IDs. Android online service calls use the native audience/device-bound bridge; direct app-link context is currently web-only. ## Managed actions Actions declare `namespace`, a closed `input_schema`, `role` (`authenticated`, `viewer`, `editor`, `manager`), and one to eight `steps`. With no group, `manager` means the app's owner; with a group it requires current manager membership. Steps use `create`, `update`, `archive`, `delete`, a declared `collection`, optional `id`, `data`, numeric `increments`, `conditions`, and `expected_revision`. Conditions are `[field,comparison,value]`. All conditions/effects run in one transaction. Values can reference `{"input":"field"}`, `{"actor":true}`, an earlier `{"result":"0.id"}`, or the current condition's `{"record":"capacity"}`. These are closed value references, not JavaScript expressions. There is no code evaluation, arbitrary filesystem/database access or outbound network execution. Use group roles for restricted operations; an `authenticated` action is callable by every signed-in eligible app user. Managed updates cannot read or disclose another actor's unreadable private record. The bookings example atomically increments an event only while `count < capacity` and inserts a unique `(event,actor)` registration. Failure rolls back both steps. The quiz example labels client-reported results honestly; database integrity does not establish independent anti-cheat or validate an arbitrary game engine. ## Anonymous forms, app inbox and scheduling A collection can declare `anonymous:true` only with `read:"managers"` and `write:"actions"`. `submit(data)` then accepts a bounded, launch-scoped create without returning a record ID. Guests cannot enumerate results, assign ownership, select a group or invoke authenticated actions. Existing general request limits, ten native requests per guest launch/minute, daily submission limits and project quotas apply. This is a limited public-form mode, not general guest write access. Actions can declare a fixed `notify:{title,group?:true}` event. Recipient scope is the actor or the authorized resource group; arbitrary account recipient lists, message HTML and contact details are not accepted. `Hackemist.notifications` offers `list`, `mark(id,{dismiss?})`, and `preferences(enabled)`. Membership is rechecked on reading. Delivery receipts report created/suppressed counts, honoring preferences and limits. This is an app inbox; browser push, email and SMS are not silently enabled. Existing platform release/Journal notifications remain intact. Actions explicitly marked `schedulable:true` can run through `Hackemist.jobs.schedule({action,input,at,intervalSeconds?,runs?,group?})`. `at` is UTC epoch seconds; recurrence is an absolute UTC interval, not local civil time or cron, so daylight-saving shifts do not change the interval. Minimum repeat interval is one hour, maximum 100 runs, with the complete requested schedule inside thirty days. Missed intervals are skipped rather than replayed in a storm. The existing operations worker handles at most four due actions per pass. Access, membership, entitlements and the exact action-definition hash are rechecked. Cancel/status/list are owner-scoped. Effects, notification entries, receipts and job advancement commit together. Status does not imply a browser is still open. Failed definition/access/validation checks stop the job; confirmed database-only aborts may retry, while unknown commit outcomes are not blindly replayed. An app can declare a manager-only delete action for private form submissions or protected records. It must provide the deletion UI explicitly; publication does not automatically erase records. The survey example includes this retention path. No job can run custom code, arbitrary URLs, media conversion or a chargeable AI call. Existing AI jobs and their credit/unknown-outcome rules remain separate. Offline restore cancels pending native schedules; users explicitly reschedule. ## Retry, quotas and lifecycle `Hackemist.operationId()` includes launch-supplied server UTC issuance time and a random ID. Keep it with the identical payload across uncertain responses. Repeating it returns the committed receipt; a changed payload rejects. IDs expire after seven days, after which the client must reconcile records before creating a new operation. Expired IDs never execute again. Receipts are retained through the retry window and bounded separately; success is returned only after durable commit. Replays recheck current disclosure rights and cannot restore revoked membership. Do not substitute a fresh ID merely because a response was lost. Default hard bounds per project/environment are 5,000 records / 8 MiB of record JSON, 20,000 receipts / 8 MiB of receipt results, 100 groups, 100 members/group, 200 unexpired invitations, 100 pending jobs (10/actor), 1,000 retained jobs and 10,000 app inbox rows. Records are at most 16 KiB; runtime envelopes at most 64 KiB; transactions at most eight steps. Notification limits are 100/actor/day and 1,000/project/day and remain charged after dismissal. Each actor inbox retains at most 1,000 items. Anonymous submissions are at most 100/collection/day. Query limits and full receipts are discoverable via `Hackemist.capabilities()`. Quotas fail explicitly; no unlimited storage or concurrency is promised. Native HTTP uses a separate two-thread, 32-request/2-MiB admission lane per control process. It shares existing SQL pools and a short per-project/environment writer guard; it does not multiply the save workers or introduce an always-on compute service. Rendering and binary transfer remain outside this lane. The operator can disable native services globally in Admin; existing saves are unaffected. Compatible releases automatically accept additive optional schema properties. Changing/removing existing property definitions, introducing new required fields or altering existing indexes or access policies fails with `SERVICE_MIGRATION_REQUIRED`. Removed collections retain their compatibility history so a later release cannot reinterpret their data or widen private access. Schema history is bounded to 128 KiB per project/environment. Use a new collection/namespace, explicitly export authorized old records, transform them client-side and write validated replacements. Preserve originals until the owner verifies the migration. No server executes migration scripts. New namespaces still share the project quota. Rollback must be compatible with retained data; restoring old code never rewinds shared revisions or deletes newer records. Studio previews use a separate `preview` environment; production is `live`. Standalone MCP/checker previews cannot access either account or shared data. Fixture records can be created and explicitly deleted in the authenticated Studio preview. Creator tools inspect schemas/usage only. Ordinary authorized queries provide cursor-based data export; account export includes authored records. Account deletion erases authored native content, memberships and actor mappings, while retaining anonymous archived record tombstones so shared references do not become a different record. Other actors' saved operation results containing that content are redacted; replay reports `SERVICE_RECORD_ERASED` without executing a second effect. Unpublishing/project removal blocks access and retains data under the platform's existing retention policy. Remixing into another project never copies private runtime data. `SERVICE_NOT_DECLARED`, `SERVICE_AUTH_REQUIRED`, `SERVICE_READ_ONLY`, `SERVICE_VERSION_UNSUPPORTED`, `SERVICE_CONFLICT`, `SERVICE_OPERATION_CONFLICT`, `SERVICE_OPERATION_EXPIRED`, `SERVICE_MIGRATION_REQUIRED`, `SERVICE_QUOTA_EXCEEDED` and `SERVICE_DISABLED` distinguish common failures. Preserve pending edits on every failure. Capability metadata is not a browser probe or completed acceptance. ## Offline helpers, examples and verification `examples/native-services/shared_outbox.js` is an optional local library. Bundle it only when needed. It coordinates up to fifty edits / 256 KiB with Web Locks, binds them to project/actor/environment/namespace, retries immutable IDs and stops on conflicts. It never queues managed actions/bookings/jobs or claims cloud success before acknowledgement. The reserved `hackemist:shared-pending:` journal is preserved separately and never included in personal browser-save uploads. Export/discard are explicit. Browser storage can be evicted or partitioned by a policy/save-slot origin change; it is not a backup or universal IndexedDB synchronization. The existing native personal-save outbox continues to handle Android saves independently. Five self-contained static examples live under `examples/native-services/`: `tasks`, `bookings`, `survey`, `notes`, `quiz`. Upload one example's folder contents at the build root. Each uses the same backend implementation. Notes demonstrate revision-checked snapshots, not CRDT text merging. Attachments use an app's own approved external backend. The quiz also uses `createProgressStore` for personal progress: enable isolated browser storage in Studio → App details → Browser compatibility before launching it, so its durable local recovery cache is available. This requires the deployment's configured isolated runtime origins. Run `python -m scripts.prepare_native_examples` to rebuild examples; `static/sdk.d.ts` describes the injected native service APIs. SQLite/Linux regression: `PYTHONPATH=tests python -m unittest test_native_services test_creator_connections -v`. Native PostgreSQL uses the repository's explicitly named disposable fixture and `scripts.check_postgres test_native_services`. Client checks: `node --test tests/native_services_sdk.spec.mjs`. The opt-in browser fixture and its real HTTP/WebSocket engine are defined by `scripts/native_services_fixture.py` and `tests/browser_native_services.spec.mjs`; they refuse normal production settings. Physical Android hardware, public WAN, provider storage throughput and the 10,000-user target require separate evidence. SDK 1.11 adds [native referees and competitions](/docs/native-sessions.md). All numerical operating budgets in this document describe defaults; administrators can change them in [Limits and capacity](/docs/resource-limits.md). Apps must use returned capabilities instead of copying these numbers into a second resource ceiling.