If you want to see the final code you can refer to it here: Final Demo Code
This guide walks you through building a geospatial hotel search demo that finds hotels within a specified distance from airports. The application uses AWS AppSync (GraphQL) with environment variables for credential management, Couchbase Data API for executing SQL++ queries, and a Streamlit frontend for interactive map visualization — end to end, with inlined code.
What is Data API? Couchbase Data API provides a RESTful HTTP interface to your cluster. Instead of embedding the Couchbase SDK in your app, you make standard HTTP requests to query, insert, or update documents. This is perfect for serverless architectures (like AppSync) because:
Learn more: Data API vs. SDKs
Steps:
https://your-cluster.apps.cloud.couchbase.com). Keep your Couchbase username/password handy.travel-sample) is loaded and accessible.Notes:
travel-sample.inventory.hotel and travel-sample.inventory.airport collections using a geospatial distance calculation.Authorization header).Why AppSync? AppSync provides a managed GraphQL layer with built-in auth, and logging. It lets your frontend speak GraphQL while your backend (Data API) speaks SQL++. The resolver bridges the two.
Steps:
This schema defines:
Hotel: type matching the Travel Sample hotel documents with all fields including geo location and reviews.Airport: type with name and nested location (GeoObject) representing airport information.GeoObject: shared type for latitude, longitude, and accuracy used by both hotels and airports.Output: response type that returns both hotels array and airport object.Query.listHotelsNearAirport: the main query that takes an airport name and distance in km, returns hotels within that radius plus the airport information.type Airport {
location: GeoObject
name: String
}
type GeoObject {
accuracy: String
lat: Float
lon: Float
}
type Hotel {
address: String
alias: String
checkin: String
checkout: String
city: String
country: String
description: String
directions: String
email: String
fax: String
free_breakfast: Boolean
free_internet: Boolean
free_parking: Boolean
geo: GeoObject
id: Float
name: String
pets_ok: Boolean
phone: String
price: String
public_likes: [String]
reviews: [HotelReviewObject]
state: String
title: String
tollfree: String
type: String
url: String
vacancy: Boolean
}
type HotelRatingObject {
Cleanliness: Float
Location: Float
Overall: Float
Rooms: Float
Service: Float
Value: Float
}
type HotelReviewObject {
author: String
content: String
date: String
ratings: HotelRatingObject
}
type Output {
hotels: [Hotel]
airport: Airport
}
type Query {
listHotelsNearAirport(airportName: String!, withinKm: Int!): Output
}
schema {
query: Query
}What's an HTTP data source? AppSync can call external HTTP APIs. You configure a base URL (your Data API endpoint), and resolvers send requests to it.
Steps:
Why environment variables? Storing credentials as environment variables in AppSync keeps them centralized. This approach:
Steps:
cb_username, Value: Your Couchbase usernamecb_password, Value: Your Couchbase passwordThese environment variables will be accessible in your resolvers via ctx.env.cb_username and ctx.env.cb_password.
Query.listHotelsNearAirportWhat does this resolver do? AppSync resolvers have two functions:
request() — transforms the GraphQL request into an HTTP request to send to the data source.response() — transforms the HTTP response from the data source back into GraphQL data.Our resolver:
cb_username and cb_password from AppSync environment variables (ctx.env).airportName and withinKm from the GraphQL arguments./_p/query/query/service (Data API's SQL++ query endpoint).Authorization: Basic <base64(username:password)>, Content-Type: application/json.{ query_context: "default:travel-sample.inventory", statement: "...", args: [airportName, withinKm], timeout: "30m" }.Output schema format.Why query_context?
Setting query_context to default:travel-sample.inventory lets you write FROM hotel instead of the fully qualified FROM travel-sample.inventory.hotel in your SQL++. It's a namespace shortcut.
Why environment variables for credentials? Storing credentials as environment variables in AppSync means:
Paste this code:
import { util } from '@aws-appsync/utils';
export function request(ctx) {
// Read credentials from AppSync environment variables
const username = ctx.env.cb_username;
const password = ctx.env.cb_password;
// Define the Couchbase keyspace (bucket.scope.collection)
const bucket = "travel-sample";
const scope = "inventory";
// Build Basic auth header for Data API
// Data API expects: Authorization: Basic <base64(username:password)>
const token = util.base64Encode(`${username}:${password}`);
const auth = `Basic ${token}`;
// Construct a geospatial SQL++ query using a Common Table Expression (CTE)
// The query:
// 1. WITH clause: Finds the airport by name and extracts its coordinates
// 2. Main SELECT: Joins hotels with airport location and calculates distance
// 3. WHERE clause: Filters hotels within the specified radius using Pythagorean theorem
// Using $1, $2 as positional parameters for security (prevents SQL injection)
const sql_query = `
WITH airport_loc AS (
SELECT a.geo.lat AS alat,
a.geo.lon AS alon,
IFMISSINGORNULL(a.geo.accuracy, "APPROXIMATE") AS accuracy
FROM airport AS a
WHERE a.airportname = $1
LIMIT 1
)
SELECT h.*, airport_loc.alat, airport_loc.alon, airport_loc.accuracy
FROM hotel AS h, airport_loc
WHERE airport_loc.alat IS NOT MISSING
AND POWER(h.geo.lat - airport_loc.alat, 2)
+ POWER(h.geo.lon - airport_loc.alon, 2) <= POWER($2 / 111, 2)
`;
// Log to CloudWatch for debugging (best practice)
console.log("Request Context:", ctx);
// Build the HTTP request object for the Data API Query Service
const requestObject = {
method: 'POST',
resourcePath: '/_p/query/query/service', // Data API SQL++ endpoint - see https://docs.couchbase.com/cloud/data-api-reference/index.html#tag/Query
params: {
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': auth // Basic auth using environment variables
},
body: {
query_context: `default:${bucket}.${scope}`, // Namespace shortcut
statement: sql_query, // The geospatial SQL++ query
args: [ctx.arguments.airportName, ctx.arguments.withinKm], // Positional parameters
timeout: '30m' // Query timeout
}
}
};
// Log the outgoing request
console.log("Outgoing Request to Data API:", requestObject);
return requestObject;
}
export function response(ctx) {
// Log the complete response context
console.log("Response Context:", ctx);
// Data API returns JSON like: { results: [ {...hotel1...}, {...hotel2...} ], ... }
// Each result contains hotel fields plus airport coordinates (alat, alon, accuracy)
let parsedResult = ctx.result.body;
if (typeof ctx.result.body === 'string') {
parsedResult = JSON.parse(ctx.result.body);
console.log("Parsed Result:", parsedResult);
}
const results = parsedResult.results || [];
// Extract airport information from the first result (all results have the same airport location)
let airport = null;
if (results.length > 0) {
const first = results[0];
airport = {
name: ctx.arguments.airportName, // Use the input airport name
location: {
lat: first.alat,
lon: first.alon,
accuracy: first.accuracy
}
};
}
// Clean up hotels by removing airport location fields (alat, alon, accuracy)
// These were added by the JOIN but aren't part of the Hotel schema
const hotels = results.map(hotel => {
const { alat, alon, accuracy, ...cleanHotel } = hotel;
return cleanHotel;
});
// Return in the Output schema format with both hotels and airport
return {
hotels: hotels,
airport: airport
};
}Key takeaways:
ctx.env provides access to AppSync environment variables (credentials stored securely).ctx.arguments gives you the GraphQL args: airportName and withinKm.util.base64Encode is an AppSync helper to encode credentials.resourcePath is the API endpoint path relative to your HTTP data source base URL. In this case, /_p/query/query/service is the Data API Query Service endpoint for executing SQL++ queries.query_context sets the default bucket/scope for SQL++, allowing short names like hotel instead of travel-sample.inventory.hotel.$1, $2) in the SQL++ query prevents SQL injection.Airport object from the result data and input arguments.Save and deploy your resolver.
Why enable logging? CloudWatch logs let you see exactly what your resolver sends to Data API and what it receives back. This is invaluable for debugging:
statement in the request log.Authorization header and Data API response.Steps:
Now, every resolver invocation will log to CloudWatch. You can view logs in the CloudWatch console under the log group you selected.
Test your resolver directly in the AppSync console before building the frontend.
query ListHotelsNearAirport {
listHotelsNearAirport(
airportName: "Les Loges"
withinKm: 50
) {
hotels {
id
name
address
city
country
phone
price
url
geo { lat lon }
reviews { ratings { Overall } }
}
airport {
name
location {
lat
lon
accuracy
}
}
}
}Try different airport names like "San Francisco Intl", "Charles de Gaulle", "Luton", or "London St Pancras" to see results from different locations.
Troubleshooting: If you encounter a "connection timed out" error, verify that your Data API has IP address access set to "Allow access from anywhere" in the Capella Connect page (see Prerequisites section). This is required for AWS AppSync to reach your cluster.
If it works, you've successfully bridged AppSync → Data API → Couchbase with geospatial querying!
Note: The credentials are read from the environment variables you configured earlier, so you don't need to pass them in the query.
Now that you've set up your AWS AppSync backend with the Couchbase Data API integration, you can test it immediately using our hosted Streamlit frontend—no need to install or run anything locally!
Visit the live Streamlit app at https://couchbase-data-api-appsync-demo.streamlit.app/
The app provides an interactive map-based interface where you can search for hotels near airports. Simply enter your AppSync GraphQL Endpoint and AppSync API Key in the sidebar (note that Couchbase credentials are securely stored as environment variables in AppSync, so you don't need to enter them). Click the "Search Hotels" tab, enter an airport name like "San Francisco Intl", "Les Loges", "Luton", or "London St Pancras" and a distance like 100 km, then click "Search". A loading spinner will appear while fetching results.
Results display on an interactive map with the airport shown as an orange marker and hotels as color-coded dots (green for excellent ratings, red for poor). The map automatically centers on the airport location. Hover over any marker to see details like hotel names, addresses, prices, and phone numbers, or expand the "Raw response" section to see the full JSON data from AppSync.
Want to explore the source code or run it locally? The complete Streamlit frontend code is available at https://github.com/couchbase-examples/couchbase-data_api-appsync-demo/tree/main/src/frontend. The frontend consists of home.py (tab-based navigation and connection settings) and search_hotels.py (hotel search with map visualization using pydeck). It uses the requests library to call AppSync GraphQL with standard HTTP POST requests. Ratings are computed from review data and mapped to colors for visual feedback.
To run locally:
# Clone the repository
git clone https://github.com/couchbase-examples/couchbase-data_api-appsync-demo.git
# Navigate to the project directory
cd couchbase-data_api-appsync-demo
# Create a virtual environment
python3 -m venv .venv
# Activate the virtual environment
source ./.venv/bin/activate
# Install required dependencies
pip install -r requirements.txt
# Run the Streamlit app
streamlit run src/frontend/home.pyThe app will start at http://localhost:8501.
You might wonder why we chose this particular stack instead of calling Couchbase Data API directly from the frontend. Here's the reasoning behind each technology choice:
AppSync as an API Gateway provides credential security by storing your Couchbase credentials as environment variables on the server side—your frontend never sees or handles database credentials. It also acts as a unified gateway that can aggregate data from multiple sources as your application grows. You get built-in authentication, authorization, request throttling, caching, and CloudWatch logging without building these features yourself.
GraphQL over REST gives clients the flexibility to request exactly the data they need, reducing bandwidth and improving performance. Instead of multiple REST endpoints, you have one endpoint with a strongly-typed schema that serves as self-documenting API documentation. The schema can evolve over time by adding new fields without breaking existing queries.
Streamlit for the frontend enables rapid development with pure Python—no HTML, CSS, or JavaScript required. It's perfect for prototypes and internal tools, with built-in support for maps (via pydeck), charts, and tables. You get direct access to data science libraries like pandas and easy integration with any HTTP API. Plus, Streamlit apps can be deployed to Streamlit Cloud in minutes.
The architecture summary: Frontend (Streamlit) → API Layer (AppSync GraphQL) → Data API (Couchbase) → Database (Capella). This separates concerns so each layer can be developed, tested, and scaled independently.
You've successfully built a complete demo showcasing Couchbase Data API integration with AWS AppSync GraphQL and Streamlit—creating a serverless geospatial hotel search application that executes SQL++ queries through AppSync resolvers, manages credentials securely via environment variables, and visualizes results on interactive maps with color-coded ratings and dual layers for hotels and airports.