# AutoSugges — Add AutoSugges autocomplete to an Expo app

## How do I add autocomplete to an Expo or React Native app?

Create the same client with createAutosuggesClient, call bootstrap() once, and drive the useAutosuggesCombobox hook from @autosugges/client/native. Spread its accessibility props onto your own TextInput, FlatList and rows. The native binding provides hooks and prop mappers rather than components, so you keep full control of layout and styling.

You are integrating AutoSugges, a hosted autocomplete and list-of-values service. Everything you need is in this message; do not guess at anything it does not state, and do not fetch documentation to fill a gap — ask instead.

## Objective
A working, screen-reader-correct autocomplete in an Expo app, using the same client and the same query policy as the web surfaces.

## Environment
- Target framework/runtime: Expo / React Native.
- SDK package: `@autosugges/client` (version 0.1.1).
- Runtime HTTP API version: `v1`.
- Install: npm install @autosugges/client

## Prerequisites
- An AutoSugges list that has been published at least once. An unpublished list has nothing to serve yet and returns `list_not_published`.
- The publishable key issued for the application that will query it.
- The runtime base URL for the environment you are targeting. AutoSugges does not have one fixed public hostname baked into the SDK — the dashboard's integration panel shows the origin for your environment.
- An Expo (or bare React Native) project with React 18 or newer.

## Values you must supply
- `baseUrl` (required, public by design) — placeholder `YOUR_RUNTIME_BASE_URL`. The origin of the AutoSugges runtime — scheme and host, no trailing slash and no path. The SDK appends `/v1/...` itself. Source: The AutoSugges dashboard's integration panel, for the environment you are deploying to.
- `publishableKey` (required, public by design) — placeholder `YOUR_PUBLISHABLE_KEY`. Identifies the consumer, the application, the subscription, the canonical list and the query policy in a single server-side lookup. The client never supplies a list id, tenant id or version hash — if a generated integration is passing one of those, it is wrong. Source: The AutoSugges dashboard, under the application that will make the queries.
- `accessToken` (optional, SECRET — server-side only) — placeholder `YOUR_SERVER_MINTED_ACCESS_TOKEN`. Only for a list whose policy requires a token. Short-lived, sent as `authorization: Bearer <token>`, and never minted, stored or hardcoded in client code. Source: Your own backend, which mints it from your AutoSugges credentials.
- The base URL below is still the literal placeholder `YOUR_RUNTIME_BASE_URL`. Ask the developer for their environment's runtime origin and substitute it; do not invent an autosugges.com hostname.

## Request contract
The publishable key travels as a query parameter (or, for selection reports, a JSON body field) — NOT as an Authorization header. The Authorization header is reserved for the optional access token of a protected list.
- `GET /v1/bootstrap?key=<publishable key>` — Resolves the list identity, the active version hash and the effective query policy for this key. Call once per session before querying. Key placement: `key` query parameter. Headers: `x-request-id`: a per-request correlation id (the SDK mints one automatically); `authorization: Bearer <access token>`: optional, and only for a list whose policy requires a token.
- `GET /v1/query?key=<publishable key>&q=<text>&locale=<optional BCP-47 tag>` — Returns the pre-ranked suggestions for one query. Key placement: `key` query parameter. Headers: `x-request-id`: a per-request correlation id; `authorization: Bearer <access token>`: optional, as above.
- `POST /v1/selection` — Reports which suggestion the user chose, so ranking can improve. Optional: skipping it degrades ranking quality and nothing else. Key placement: `key` field of the JSON body, alongside `selectionId`, `queryRequestId`, `listId`, `itemId`, `query` and `position`. Headers: `content-type: application/json`; `x-request-id`.

## Policy that arrives from bootstrap — never hardcode these
- `requestId` (string) — Correlation id for this bootstrap call.
- `list.id` (string (UUID)) — The canonical list this key resolves to. Never supplied by the client.
- `activeVersionHash` (string) — The published version currently serving. Compare it to a stored hash to decide whether a cached local copy is stale.
- `queryPolicy.minQueryChars` (number) — Minimum characters before a query is legal. Never below 2, and never hardcoded — a shorter query is rejected and not billed.
- `queryPolicy.debounceMs` (number) — How long to wait after the last keystroke before issuing a query.
- `queryPolicy.defaultResultLimit` (number) — How many suggestions a list returns by default. Informational: the server already returns its ranked set, so the client does not send this back.
- `queryPolicy.rememberLastSelection` (boolean) — Tenant policy for prefilling a previously chosen item. Treat anything other than literal `true` as off.

## What one suggestion looks like
- `itemId` (string) — Stable identifier. Use it as the React/list key.
- `value` (string) — The canonical value — what you store.
- `displayValue` (string) — The locale-resolved label — what you render. Not the same field as `value`.
- `displayOrder` (number) — The owner's configured display order.
- `position` (number) — 1-based position in the returned ranking.
- `ancestors` (array, optional) — The precomputed hierarchy chain, root first, excluding the item itself — `{ itemId, value, displayValue, hierarchyLevel? }`. Use it to auto-fill parent form fields. Absent when the item has no ancestors; never an empty array.

## Implementation steps
1. Create one client — createAutosuggesClient works unchanged in React Native: its transport is the standard Fetch API, which the runtime provides natively. No polyfill.
2. Bootstrap once — Call client.bootstrap() at startup so minChars and debounce come from the list's policy rather than a guessed constant.
3. Drive the hook — useAutosuggesCombobox({ source, onSelect, onError }) owns query, debounce, cancellation and active-option state. Do not reimplement any of it.
4. Spread the accessibility props — Spread getInputAccessibilityProps(state), getListAccessibilityProps() and getOptionAccessibilityProps(state, index) onto your TextInput, FlatList and rows. @autosugges/client/native exports mappers and hooks, never components — React Native has no DOM, so the consumer owns the elements.
5. Announce explicitly on iOS — iOS has no live region. Speak activeOptionAnnouncement(state) and state.announcement through AccessibilityInfo.announceForAccessibility in effects.

## Code — `CityField.tsx`
```tsx
import { useEffect, useState } from 'react';
import { AccessibilityInfo, FlatList, Pressable, Text, TextInput } from 'react-native';
import { createAutosuggesClient } from '@autosugges/client';
import {
  activeOptionAnnouncement,
  getInputAccessibilityProps,
  getListAccessibilityProps,
  getOptionAccessibilityProps,
  handleNativeKeyPress,
  useAutosuggesCombobox,
} from '@autosugges/client/native';

// The SDK's transport is the standard Fetch API, which React Native provides
// natively. No polyfill, and no native module.
const autosugges = createAutosuggesClient({
  baseUrl: 'YOUR_RUNTIME_BASE_URL',
  publishableKey: 'YOUR_PUBLISHABLE_KEY',
});
void autosugges.bootstrap().catch(() => {});

export function CityField() {
  const [city, setCity] = useState('');
  const [errorCode, setErrorCode] = useState<string | undefined>();

  // /native exports hooks and prop mappers, not components: React Native has no
  // DOM, so the consumer owns the elements and the binding owns their semantics.
  const combobox = useAutosuggesCombobox({
    source: autosugges,
    onSelect: (item) => setCity(item.value),
    onError: (error) => setErrorCode(error.code),
  });
  const { state } = combobox;

  // iOS has no live region, so announcements are spoken explicitly.
  const spoken = activeOptionAnnouncement(state);
  useEffect(() => {
    if (spoken !== undefined) AccessibilityInfo.announceForAccessibility(spoken);
  }, [spoken]);

  return (
    <>
      <TextInput
        {...getInputAccessibilityProps(state)}
        accessibilityLabel="City"
        value={state.inputValue}
        onChangeText={combobox.setInputValue}
        onKeyPress={(event) => handleNativeKeyPress(combobox, event.nativeEvent.key)}
      />
      <FlatList
        {...getListAccessibilityProps()}
        data={state.items}
        keyExtractor={(item) => item.itemId}
        renderItem={({ item, index }) => (
          <Pressable
            {...getOptionAccessibilityProps(state, index)}
            accessible
            onPress={() => combobox.selectIndex(index)}
          >
            <Text>{item.displayValue}</Text>
          </Pressable>
        )}
      />
    </>
  );
}
```

## Security requirements
- A publishable key is public by design. It is safe in client-side JavaScript, in a mobile bundle, and in a page a search engine can read. It is not a secret and does not need a proxy.
- A publishable key is not a substitute for origin control: keys are bound to an allow-list of origins, and a request from an origin the tenant has not allowed is refused with `origin_not_allowed` and is not billed. Add the deploying site’s origin in the AutoSugges dashboard before going live.
- Never place a secret key, a signing key, a service-role credential or a database URL in client code, in an environment variable prefixed for browser exposure, or in a prompt. Nothing in an AutoSugges browser or mobile integration needs one.
- An access token (`authorization: Bearer …`) is only for a list whose policy requires one. It is short-lived, and it is minted by your own server from your own AutoSugges credentials — never in the browser, never hardcoded, never committed.
- Do not log the raw query text or the response body. Log the typed error `code` and the `requestId`.
- A mobile bundle is not a secret store: anything embedded in the app can be extracted. Only the publishable key and the base URL belong there.

## Expected result
Typing into the field returns suggestions from the published list, the control is fully operable and announced by the platform screen reader, and selecting a row fires onSelect with the canonical item.

## Validation — do these before reporting success
1. Check 1 — Run on a device or simulator and confirm suggestions appear after the configured debounce.
2. Check 2 — Enable VoiceOver (iOS) or TalkBack (Android) and confirm the input announces its expanded state, the active option, and the result count.
3. Check 3 — Confirm each touch target meets the 24×24dp minimum — use the binding’s hit-slop helper where the visual size is smaller.
4. Check 4 — Put the device on a slow or offline connection and confirm the error state renders rather than the app hanging or crashing.
5. Check 5 — Confirm no request is issued below minQueryChars.

## Error codes you must handle
Every failure arrives as a typed code in the response body (and, through the SDK, as an `AutosuggesError` with that `code`). Switch on the code; never match on a message string. The codes most likely on this surface are: `invalid_key`, `list_not_published`, `quota_exceeded`, `artifact_unavailable`. The complete vocabulary:
- `invalid_key` (HTTP 401, not retryable) — The publishable key is wrong, revoked or rotated. Check the key in the AutoSugges dashboard. Do not retry with the same key.
- `origin_not_allowed` (HTTP 403, not retryable) — The requesting origin is not on this key's allow-list. Add the origin in the dashboard. Not billed.
- `invalid_list` (HTTP 404, not retryable) — The key resolves to no list, or the list was deleted. Nothing to retry.
- `subscription_required` (HTTP 403, not retryable) — The list is shared and this consumer has no active subscription to it. Subscribe, or use a list you own.
- `list_not_published` (HTTP 409, not retryable) — The list exists but has never been published, so there is no artifact to serve. Publish it once from the dashboard.
- `token_expired` (HTTP 401, not retryable) — The access token is past its expiry. Mint a fresh one on your server and retry once. Never extend expiry on the client.
- `invalid_token` (HTTP 401, not retryable) — The access token failed signature or scope verification. Mint a new one; do not retry the same token.
- `quota_exceeded` (HTTP 200, not retryable) — The allowance for this billing scope is spent. `details.scope` says which allowance — `subscription` or `key`. The response carries no items at all, real or synthetic: render an upgrade path, and do not feed it into selection analytics. It clears at the next billing period.
- `query_too_short` (HTTP 200, not retryable) — The query is below the list's `minQueryChars` (`details.minQueryChars`). Do not issue the request at all until the input reaches it. Not billed.
- `query_too_long` (HTTP 400, not retryable) — The query exceeds the server maximum (`details.maxQueryChars`). Trim the input before sending; this is a malformed request, not something a user types.
- `invalid_request` (HTTP 400, not retryable) — The request is malformed — a missing parameter or an unusable value. `details` names what. Fix the call; retrying unchanged cannot help.
- `rate_limited` (HTTP 429, retryable) — Too many requests for this key, subscription or IP. Honour the `retryAfter` seconds in the body before retrying, and check that the input is actually debounced.
- `challenge_failed` (HTTP 403, not retryable) — A bot challenge was not satisfied on a session-establishing surface. Re-present the challenge. Not billed.
- `publish_validation_failed` (HTTP 422, not retryable) — A publish was rejected by validation (duplicates, an unresolved parent, a hierarchy cycle). The previously active version keeps serving. Fix the data and publish again.
- `compile_failed` (HTTP 500, retryable) — Compilation of a new version failed. The previously active version keeps serving; nothing at runtime changed. Retry the publish after fixing the reported cause.
- `artifact_unavailable` (HTTP 503, retryable) — A published artifact could not be read at this edge, usually mid-propagation. Retryable: wait the `retryAfter` seconds and retry once. Do not render a partial or stale result set alongside a fresh one.
- `resource_removed` (HTTP 404, not retryable) — The resource was deleted. Stop requesting it; there is nothing to retry.
- `invalid_domain` (HTTP 400, not retryable) — The submitted domain or slug is not valid or is reserved. Choose another.
- `domain_validation_failed` (HTTP 409, not retryable) — A custom-domain DNS or TLS validation did not complete. Re-check the DNS records shown in the dashboard, then revalidate.
- `forbidden` (HTTP 403, not retryable) — Your account is signed in but your workspace role does not permit this action. Ask an owner or admin to change your role, or use an account that has it.
- `internal_error` (HTTP 500, retryable) — An unexpected server-side failure. Report the `requestId` from the response. Retry at most once, with backoff.

## Notes
- Never hardcode a minimum query length or a debounce interval. Both come from `client.policy`, which the SDK fills from the bootstrap response and updates in place. The constants the SDK falls back to before the first successful bootstrap are documented defaults, not the list’s real policy.
- Render `displayValue`, store `value`. They differ: `displayValue` is resolved for the requested locale at compile time.
- A selection report is optional. Omitting it costs ranking quality over time and nothing else; it is never required for a query to work.
- There is no origin header on a native request, so origin allow-listing does not protect a mobile app. Quota, rate limiting and (for protected lists) access tokens are what bound abuse there.

## Reference
- Expo integration guide: https://docs.autosugges.com/integrations/expo
- All integration guides: https://docs.autosugges.com/integrations

## Frequently asked questions

### What is the minimum number of characters before a query runs?

Two by default. A list owner can change the minimum, but it can never be 1. The client reads the real value from bootstrap and sends no request below it, so do not hardcode it.

### How long does the client wait after typing before it queries?

150 milliseconds by default. The value is delivered through bootstrap and a list owner can raise it, so the client uses the policy value rather than a constant.

### Where does the publishable key go?

In the key query parameter of the request, for example /v1/query?key=YOUR_PUBLISHABLE_KEY, never in a header. The key is public by design and safe in client code. Secret keys never belong in a browser or mobile bundle, and a proxy to hide the publishable key is unnecessary.

### What happens when my query quota runs out?

The runtime returns a structured quota_exceeded response that carries no items and no placeholder suggestions, so exhaustion is never mistaken for no results. Render it from the typed error code and prompt an upgrade.

### Does AutoSugges work in React Native?

Yes. The same client runs in React Native because its transport is the standard Fetch API, and @autosugges/client/native provides hooks and accessibility prop mappers for Expo and bare React Native apps. See the Expo guide.

### Does origin allow-listing protect a mobile app?

No. A native request carries no Origin header, so quota, rate limiting and, for protected lists, access tokens are what bound abuse in an app.

---
Canonical page: https://docs.autosugges.com/integrations/expo
