# AppSync **Protocol:** REST JSON **Endpoint:** `http://localhost:4566/v1/apis/...` Floci implements the AWS AppSync Management API, providing local emulation of GraphQL API configuration, schema management, data source binding, resolver mapping, API key provisioning, custom domains, and channel namespaces. ## OIDC issuer network policy AppSync OIDC authentication uses the shared JWT issuer policy. By default, issuer discovery and JWKS requests require HTTPS and reject local, private, link-local, and other non-public addresses. For an isolated development environment, set `FLOCI_SECURITY_ALLOW_PRIVATE_JWT_TARGETS=true`. This also applies to API Gateway HTTP API JWT authorizers. The option permits private HTTPS targets and HTTP URLs that use a literal private or loopback address. ## Supported Operations ### GraphQL API | Operation | Description | |---|---| | `CreateGraphqlApi` | Create a GraphQL API | | `GetGraphqlApi` | Get a GraphQL API by ID | | `UpdateGraphqlApi` | Update a GraphQL API | | `DeleteGraphqlApi` | Delete a GraphQL API and all child resources | | `ListGraphqlApis` | List all GraphQL APIs | ### Schema | Operation | Description | |---|---| | `StartSchemaCreation` | Start schema creation: validates and parses SDL via the GraphQL sidecar (invalid SDL returns 400) | | `GetSchemaCreationStatus` | Get schema creation status | | `GetIntrospectionSchema` | Get the introspection schema | ### Data Sources | Operation | Description | |---|---| | `CreateDataSource` | Create a data source | | `GetDataSource` | Get a data source by name | | `UpdateDataSource` | Update a data source | | `DeleteDataSource` | Delete a data source | | `ListDataSources` | List all data sources for an API | ### Resolvers | Operation | Description | |---|---| | `CreateResolver` | Create a resolver | | `GetResolver` | Get a resolver by type and field | | `UpdateResolver` | Update a resolver | | `DeleteResolver` | Delete a resolver | | `ListResolvers` | List all resolvers for an API | | `ListResolversByType` | List resolvers for a specific type | | `ListResolversByFunction` | List resolvers attached to a specific function | ### Functions | Operation | Description | |---|---| | `CreateFunction` | Create a function configuration | | `GetFunction` | Get a function by ID | | `UpdateFunction` | Update a function | | `DeleteFunction` | Delete a function | | `ListFunctions` | List all functions for an API | ### Types | Operation | Description | |---|---| | `CreateType` | Create a type | | `GetType` | Get a type by name | | `UpdateType` | Update a type | | `DeleteType` | Delete a type | | `ListTypes` | List all types for an API | ### API Keys | Operation | Description | |---|---| | `CreateApiKey` | Create an API key | | `GetApiKey` | Get an API key by ID | | `UpdateApiKey` | Update an API key | | `DeleteApiKey` | Delete an API key | | `ListApiKeys` | List all API keys for an API | As on AWS, `ApiKey.id` is the key value itself (`da2-` followed by 26 lowercase alphanumerics) and is what clients send in the `x-api-key` header. There is no separate secret field. ### Tags | Operation | Description | |---|---| | `TagResource` | Add tags to a resource | | `UntagResource` | Remove tags from a resource | | `ListTagsForResource` | List tags on a resource | ### Environment Variables | Operation | Description | |---|---| | `GetEnvironmentVariables` | Get environment variables for an API | | `PutEnvironmentVariables` | Set environment variables for an API | ### Domain Names | Operation | Description | |---|---| | `CreateDomainName` | Register a custom domain name | | `GetDomainName` | Get domain name configuration | | `UpdateDomainName` | Update domain name description | | `ListDomainNames` | List all domain names | | `DeleteDomainName` | Delete a custom domain name | | `AssociateApi` | Associate a domain name with a GraphQL API | | `GetAssociatedApi` | Get the API associated with a domain name | | `DisassociateApi` | Disassociate a domain name from a GraphQL API | | `ListApiAssociations` | List all associations for an API | ### Channel Namespaces | Operation | Description | |---|---| | `CreateChannelNamespace` | Create a channel namespace | | `GetChannelNamespace` | Get a channel namespace by name | | `UpdateChannelNamespace` | Update a channel namespace description | | `ListChannelNamespaces` | List all channel namespaces for an API | | `DeleteChannelNamespace` | Delete a channel namespace | ### Merged API Associations | Operation | Description | |---|---| | `CreateApiAssociation` | Associate a source API with a merged API | | `GetApiAssociation` | Get a merged API association | | `DeleteApiAssociation` | Delete a merged API association | | `ListApiAssociations` | List all merged API associations | ### Enhanced Metrics | Operation | Description | |---|---| | `GetEnhancedMetricsConfig` | Get the enhanced metrics configuration | ## Schema Registry Schema parsing and query execution run in the **GraphQL sidecar** (issue #2917), not in Floci's own process: [graphql-java](https://github.com/graphql-java/graphql-java) is a ~3.8MB dependency used only by AppSync, so keeping it out of Floci's JVM/native image altogether, mirroring the Cedar sidecar pattern already used for Verified Permissions, avoids paying that cost in every build regardless of whether AppSync is ever used. The sidecar is `floci/floci-sidecar-graphql`, published from [floci-io/floci-sidecars](https://github.com/floci-io/floci-sidecars) and pinned to an exact version (a cached `latest` is never re-pulled): pull it ahead of time on an air-gapped host with `docker pull floci/floci-sidecar-graphql:0.2.0`. `GraphqlSidecarManager` lazily starts the sidecar container on first use and checks the contract version it reports on `/health`; a sidecar speaking another contract major is refused immediately with an `InternalServerException` naming the image and the variable to change. `floci.services.appsync.graphql-url` points at an already-running instance instead (Docker Compose setups) and is checked the same way. The sidecar itself carries no AppSync-specific knowledge: no `@aws_auth`, no IAM, no directive semantics, and no AWS scalar vocabulary either. `StartSchemaCreation` sends the SDL (with AppSync's directive declarations and the 17 custom scalar types injected, customers never declare those themselves) plus a `scalars` mapping (each AppSync scalar name onto one of the sidecar's generic coercion kinds, e.g. `AWSDateTime` onto `date-time`) to the sidecar's `/v1/schema/validate`, which parses and compiles it generically. Invalid schemas are rejected asynchronously (status `FAILED` with details after `PROCESSING`), using the sidecar's structured, per-problem errors to build the same `codeErrors` shape this API always returned. Valid schemas are registered in Floci's own `SchemaRegistry` (now just a raw-SDL cache; nothing is compiled in-process) and persisted to the schema store. At execution time, Floci calls the sidecar's `/v1/plan` to learn which `(type, field)` coordinates a query will visit and what directives are on each, computes `@aws_auth`/IAM/Cognito/Lambda authorization decisions itself (this AWS-specific logic never runs in the sidecar), and passes any denied coordinates to `/v1/execute` as an opaque list; the sidecar nulls those fields out with the given error, without ever knowing why. On emulator startup, after storage load and orphan recovery, Floci **rehydrates** SUCCESS SDLs from the schema store into `SchemaRegistry` so `POST /v1/apis/{apiId}/graphql` works across restarts (memory/persistent/hybrid/wal), without re-validating against the sidecar, since a persisted SDL already passed validation once and restarts shouldn't eagerly start a container that might otherwise never be needed. The following **AWS scalar types** are pre-registered and available in any schema without requiring explicit `scalar` declarations: | Scalar | Java Type | Validation | |--------|-----------|------------| | `AWSJSON` | String | Valid JSON syntax | | `AWSDateTime` | String | ISO 8601 datetime | | `AWSDate` | String | ISO 8601 date (yyyy-MM-dd) | | `AWSTime` | String | ISO 8601 time | | `AWSTimestamp` | Long | Unix epoch seconds (0 to 32503680000) | | `AWSEmail` | String | RFC 5322 email format | | `AWSURL` | String | Valid URL | | `AWSPhone` | String | E.164 format (+1234567890) | | `AWSIPAddress` | String | IPv4 or IPv6 | | `AWSBoolean` | Boolean | Boolean value | | `AWSLong` | Long | 64-bit signed integer | | `AWSInteger` | Integer | 32-bit signed integer | | `AWSShort` | Integer | 16-bit signed integer (-32768 to 32767) | | `AWSFloat` | Double | IEEE 754 double-precision | | `AWSBigDecimal` | String | Arbitrary-precision decimal | | `AWSBigInt` | String | Arbitrary-precision integer | | `AWSByte` | String | Base64-encoded byte array | The following **AppSync directives** are pre-defined and recognized in schemas: | Directive | Locations | Purpose | |-----------|-----------|---------| | `@aws_api_key` | OBJECT, FIELD_DEFINITION | Require API key auth | | `@aws_iam` | OBJECT, FIELD_DEFINITION | Require IAM auth | | `@aws_cognito_user_pools(cognito_groups: [String!]!)` | OBJECT, FIELD_DEFINITION | Require Cognito user pool auth | | `@aws_oidc` | OBJECT, FIELD_DEFINITION | Require OIDC auth | | `@aws_lambda` | OBJECT, FIELD_DEFINITION | Require Lambda auth | | `@aws_subscribe(mutations: [String!]!)` | FIELD_DEFINITION | Link subscription to mutation | | `@aws_auth(cognito_groups: [String!]!)` | OBJECT, FIELD_DEFINITION | Require Cognito groups (ignored when additional auth modes exist) | | `@aws_delta_sync` | OBJECT | Delta sync configuration | Unknown directives are rejected during schema registration. Schema extensions (`extend type Query { ... }`) are supported natively through graphql-java, running in the sidecar. ## GraphQL execute (data-plane) | Surface | Path | Content-Types | |---|---|---| | HTTP GraphQL | `POST /v1/apis/{apiId}/graphql` | `application/json`, `application/graphql` (+ charset) | Execute is a **separate** data-plane endpoint from the management API. Request body is GraphQL-over-HTTP JSON: `{ "query", "variables?", "operationName?" }`. Responses are `application/json` with AWS AppSync wire shapes (`data` / `errors[]` with top-level `errorType` / `errorInfo`). Most GraphQL syntax and validation errors return **HTTP 200** with `errors[]`. | Case | HTTP | Notes | |---|---|---| | Query / introspection / validation / syntax (incl. blank `query`) | 200 | Nullable fields may be `null` until DataFetchers (Phase 8) | | HTTP subscription operation | 200 | `OperationNotSupported` (realtime WebSocket is a later phase) | | Empty body / `{}` / `[]` / unparseable JSON / bad Content-Type | 400 | `MalformedHttpRequestException` | | Missing `operationName` with multiple operations | 400 | `BadRequestException` — `Missing operation name.` | | Unknown `apiId` | 404 | `NotFoundException` | | API exists but no executable schema (incl. PROCESSING) | **502** | `GraphQLSchemaException` — `No schema definition exists.` + `x-amzn-errortype` | | Unexpected failure | 500 | `InternalFailure` | | Missing/invalid/expired credentials, unconfigured mode, Lambda deny | **401** | `UnauthorizedException` — GraphQL does not run; `x-amzn-errortype` is set. Missing headers use message `Missing authorization header`. | | Field directive mismatch, Cognito group miss, Lambda `deniedFields`, IAM field DENY | **200** | Field is `null` and `errors[]` contains `Unauthorized` — `Not Authorized to access {field} on type {type}` (no `x-amzn-errortype`) | **Evidence for data-plane statuses** (empty/`[]`/`{}` → 400; missing schema → 502): AppSync team sample in [graphql/graphql-over-http#81](https://github.com/graphql/graphql-over-http/issues/81) (@robzhu). The management API Reference lists `GraphQLSchemaException` as HTTP 400 for “schema not valid” on management operations — a different surface than the GraphQL execute data plane. ### Execute authentication Request auth runs after Content-Type and body parse and after API lookup (unknown `apiId` is still 404). It runs before schema lookup, so a missing schema with no credentials is 401, not 502. Headers are classified by **shape** (not primary-then-fallback). If both `x-api-key` and SigV4 `Authorization` are present, **SigV4 wins**. AppSync does not fall back to the API key when IAM validation fails. Missing required auth headers return HTTP **401** `UnauthorizedException` with message `Missing authorization header`. Invalid or expired credentials that are present still return 401 with `You are not authorized to make this call.` | Header shape | Mode | |---|---| | `Authorization` starts with `AWS4-HMAC-SHA256` | `AWS_IAM` (wins over `x-api-key` if both are present) | | `x-api-key` present | `API_KEY` | | `Authorization: Bearer ` | Cognito and/or OIDC (matched by `iss` / `aud` or `azp`) | | Other `Authorization` | `AWS_LAMBDA` | Configured modes are the API default `authenticationType` plus `additionalAuthenticationProviders`. A classified mode that is not configured returns 401. | Mode | Emulator notes | |---|---| | API_KEY | Lookup by `ApiKey.id`, which is the key value (`da2-…`). Identity is absent (not `{}`). Default key expiry is 7 days when `expires` is omitted; stored `expires` is rounded down to the nearest hour. Create/UpdateApiKey require `expires` between 1 and 365 days from now (`ApiKeyValidityOutOfBoundsException`, 400). `deletes` is `expires` plus 60 days. | | AWS_IAM | Verifies a real header-signed SigV4 request (`appsync` service, fixed `/v1/apis/{apiId}/graphql` canonical path, 5-minute clock skew): the `Credential=` access key must resolve to a secret via `IamService`, and the signature must match. The legacy `test`/`test` pair is still emulator ALLOW, but it must be signed with secret `test` like any other key, and it is not a bypass. A temporary (`ASIA...`) credential must also present the `X-Amz-Security-Token` header matching the one issued for it. An unknown or unsigned key is always 401 and never becomes the account-root identity. Known keys additionally evaluate `appsync:GraphQL`. | | Cognito / OIDC | JWT signature is verified, not just decoded. Cognito checks the token against the issuing user pool's own RS256 signing key (`alg`, `kid`, issuer, audience/`clientId`, expiry); OIDC checks it against the configured issuer's published JWKS (via OIDC discovery), the same way the HTTP API JWT authorizer does. Both fail closed: an unreachable issuer, unsupported algorithm (including `none`), unmatched `kid`, or bad signature is 401. OIDC as the sole mode still skips the token's own `iss` claim check, but the signature is always verified against the configured issuer's keys. OIDC identity is `{sub, issuer, claims}` (no `sourceIp`). | | Lambda | AppSync `isAuthorized` contract via `LambdaService.invoke` (not an API Gateway policy document). | SDL field auth: unmarked fields require the API **default** mode. Additional modes unlock fields tagged `@aws_api_key` / `@aws_iam` / `@aws_oidc` / `@aws_cognito_user_pools` / `@aws_lambda`. Multiple directives on a field are OR. Field-level directives override type-level. `@aws_auth` is allowed on `OBJECT \| FIELD_DEFINITION` and is ignored when additional modes exist. Duplicate `API_KEY` / `AWS_IAM` / `AWS_LAMBDA` (and the same Cognito pool or OIDC issuer) between default and additional providers is rejected on create/update with management 400 `BadRequestException`: `Authentication type {TYPE} for additional authentication provider {N} already specified on the API. It can only be specified once.` (`N` is 1-based in `additionalAuthenticationProviders`). ## Pagination All `List` operations support cursor-based pagination via query parameters: | Parameter | Description | |---|---| | `maxResults` | Maximum number of items to return | | `nextToken` | Opaque token for the next page | The `nextToken` is a Base64 URL-encoded integer offset. A missing token starts from offset 0. An invalid token returns `InvalidNextTokenException` (400). ```bash # First page aws appsync list-graphql-apis \ --max-results 10 \ --endpoint-url $AWS_ENDPOINT_URL # Next page (use the nextToken from previous response) aws appsync list-graphql-apis \ --max-results 10 \ --next-token "eyJvZmZzZXQiOjEwfQ==" \ --endpoint-url $AWS_ENDPOINT_URL ``` ## Cascade Delete Deleting a GraphQL API (`DeleteGraphqlApi`) automatically deletes all child resources: - Schema and schema creation status - All data sources - All resolvers - All functions - All types - All API keys - All channel namespaces - All domain name associations This matches AWS behavior where deleting an API removes its entire configuration. ## Not Implemented These AWS AppSync capabilities are not yet implemented and are tracked in future phases: - **DataFetcher / resolver dispatch** (Phase 8): resolver mapping templates and field resolution with non-null values - **Data source adapters** (Phase 9): DynamoDB, Lambda, HTTP, EventBridge, OpenSearch, RDS connectors - **Guardrails** (Phase 10): query depth / complexity limits and related errors - **Realtime subscriptions** (Phase 11+): WebSocket real-time subscriptions - **Caching**: API-level and per-resolver caching - **Merged API source management**: `AssociateMergedGraphqlApi`, `AssociateSourceGraphqlApi`, `StartSchemaMerge`, `ListTypesByAssociation` - **Data source introspection**: `StartDataSourceIntrospection`, `GetDataSourceIntrospection` ## Configuration | Variable | Default | Description | |---|---|---| | `FLOCI_SERVICES_APPSYNC_ENABLED` | `true` | Enable or disable the service | | `FLOCI_SERVICES_APPSYNC_VTL_MAX_LOOPS` | `10000` | Maximum `#foreach` iterations a VTL resolver template may execute | | `FLOCI_SERVICES_APPSYNC_VTL_MAX_OUTPUT_CHARS` | `1048576` | Maximum characters a VTL resolver template may render | | `FLOCI_SERVICES_APPSYNC_VTL_TIMEOUT_MILLIS` | `5000` | Maximum wall-clock time a VTL resolver template may spend evaluating | Request/response mapping templates render inside the same VTL reflection sandbox described for API Gateway in [api-gateway.md](api-gateway.md#configuration) (`SecureUberspector`, with `Class`, `ClassLoader`, `Runtime`, `ProcessBuilder`, `System`, `Thread`, `java.io.File` and related classes/packages blocked), and are subject to the same three limits above. The loop cap truncates a `#foreach` at the configured iteration count and lets the template finish rendering with whatever output it produced up to that point; it does not fail the resolver. Exceeding the output-size or execution-time limit does fail the resolver, the same way any other VTL evaluation error does. ## Examples ```bash export AWS_ENDPOINT_URL=http://localhost:4566 # Create a GraphQL API aws appsync create-graphql-api \ --name my-api \ --authentication-type API_KEY \ --endpoint-url $AWS_ENDPOINT_URL # Start schema creation aws appsync start-schema-creation \ --api-id API_ID \ --definition 'type Query { hello: String }' \ --endpoint-url $AWS_ENDPOINT_URL # Create a data source (NONE type for local resolvers) aws appsync create-data-source \ --api-id API_ID \ --name my-datasource \ --type NONE \ --endpoint-url $AWS_ENDPOINT_URL # Create a resolver aws appsync create-resolver \ --api-id API_ID \ --type-name Query \ --field-name hello \ --data-source-name my-datasource \ --endpoint-url $AWS_ENDPOINT_URL # Create an API key aws appsync create-api-key \ --api-id API_ID \ --description "Test key" \ --endpoint-url $AWS_ENDPOINT_URL # List all APIs aws appsync list-graphql-apis \ --endpoint-url $AWS_ENDPOINT_URL # Register a custom domain aws appsync create-domain-name \ --domain-name api.example.com \ --certificate-arn arn:aws:acm:us-east-1:000000000000:certificate/123 \ --endpoint-url $AWS_ENDPOINT_URL # Associate domain with API aws appsync associate-api \ --domain-name api.example.com \ --api-id API_ID \ --endpoint-url $AWS_ENDPOINT_URL # Execute a GraphQL query (data-plane; send credentials for the API auth mode) curl -s -X POST "$AWS_ENDPOINT_URL/v1/apis/API_ID/graphql" \ -H "Content-Type: application/json" \ -H "x-api-key: da2-YOUR_API_KEY" \ -d '{"query":"{ hello }"}' # Create a channel namespace aws appsync create-channel-namespace \ --api-id API_ID \ --name my-channels \ --endpoint-url $AWS_ENDPOINT_URL ```