| path | /tutorial-appsync-data-api-streamlit-travel-sample | ||||
|---|---|---|---|---|---|
| title | Build a Geospatial Hotel Search App with AWS AppSync, Couchbase Data API, and Streamlit | ||||
| short_title | Geospatial Hotel Search with AppSync & Data API | ||||
| description |
|
||||
| content_type | tutorial | ||||
| filter | sdk | ||||
| technology |
|
||||
| tags |
|
||||
| sdk_language |
|
||||
| length | 45 Mins |
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.
- Couchbase Capella account with a running cluster
- Travel-sample bucket imported into your cluster
- Data API enabled in Capella (via the cluster's Connect page)
- Important: Set Allowed IP address to "Allow access from anywhere" for demo purposes
- This configuration is required for this demo to function with AWS AppSync
- Not recommended for production—see documentation for secure network configurations
- AWS account with permissions to create AppSync APIs
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:
- Enables driverless mode: No need to deploy SDKs in FaaS environments (AWS Lambda, Azure Functions)
- Is lightweight: Avoids heavy SDK initialization overhead in stateless functions
- Simplifies integration: Uses standard HTTP—no SDK version management or dependencies
- Is language agnostic: Works with any platform that can make HTTP requests
Learn more: Data API vs. SDKs
Steps:
- In Capella, enable Data API for your project/cluster (single click in the cluster settings).
- Copy the Data API base URL (something like
https://your-cluster.apps.cloud.couchbase.com). Keep your Couchbase username/password handy. - Ensure the Travel Sample bucket (
travel-sample) is loaded and accessible.
Notes:
- We will query
travel-sample.inventory.hotelandtravel-sample.inventory.airportcollections using a geospatial distance calculation. - Credentials are configured as environment variables in AppSync and used by the resolver to call Data API.
- Data API authenticates each request via Basic auth (username:password Base64-encoded in the
Authorizationheader). - The query uses SQL++ Common Table Expressions (CTEs) to first find the airport, then calculate distances to nearby hotels.
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:
- Create an AppSync GraphQL API:
- Choose "Build from scratch" option when prompted
- Select Public API with API key authentication for demo simplicity
This schema defines:
Hotel: type matching the Travel Sample hotel documents with all fields including geo location and reviews.Airport: type withnameand nestedlocation(GeoObject) representing airport information.GeoObject: shared type for latitude, longitude, and accuracy used by both hotels and airports.Output: response type that returns bothhotelsarray andairportobject.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:
- In AppSync, create a new HTTP data source.
- Set the endpoint to your Couchbase Data API base URL (from step 1).
- Important: Do NOT enable "Authorization configuration", since credentials will be passed dynamically via the resolver using AppSync environment variables.
Why environment variables? Storing credentials as environment variables in AppSync keeps them centralized. This approach:
- Avoids exposing credentials to clients
- Makes credential rotation easier (update once in AppSync, not in every client)
Steps:
- In the AppSync console, navigate to your API → Settings → Environment variables.
- Add the following environment variables:
- Key:
cb_username, Value: Your Couchbase username - Key:
cb_password, Value: Your Couchbase password
- Key:
- Save the changes.
These environment variables will be accessible in your resolvers via ctx.env.cb_username and ctx.env.cb_password.
What 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:
- Reads
cb_usernameandcb_passwordfrom AppSync environment variables (ctx.env). - Extracts
airportNameandwithinKmfrom the GraphQL arguments. - Constructs a geospatial SQL++ query to find hotels near the specified airport.
- Builds a Data API Query Service request:
- Endpoint:
/_p/query/query/service(Data API's SQL++ query endpoint). - Headers:
Authorization: Basic <base64(username:password)>,Content-Type: application/json. - Body:
{ query_context: "default:travel-sample.inventory", statement: "...", args: [airportName, withinKm], timeout: "30m" }.
- Endpoint:
- Returns both hotels and airport information in the
Outputschema 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:
- Credentials never leave the server or get exposed to clients.
- Easy credential rotation without updating client code.
- Centralized credential management across all resolvers.
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.envprovides access to AppSync environment variables (credentials stored securely).ctx.argumentsgives you the GraphQL args:airportNameandwithinKm.util.base64Encodeis an AppSync helper to encode credentials.resourcePathis the API endpoint path relative to your HTTP data source base URL. In this case,/_p/query/query/serviceis the Data API Query Service endpoint for executing SQL++ queries.query_contextsets the default bucket/scope for SQL++, allowing short names likehotelinstead oftravel-sample.inventory.hotel.- Using positional parameters (
$1,$2) in the SQL++ query prevents SQL injection. - Distance calculation uses Pythagorean theorem approximation.
- The response function constructs an
Airportobject from the result data and input arguments. - Hotel objects are cleaned to remove the joined airport coordinate fields before returning.
- CloudWatch logging helps debug the complex geospatial query and response transformation.
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:
- Incorrect SQL++ syntax? Check the
statementin the request log. - Auth errors? Check the
Authorizationheader and Data API response. - Unexpected results? Compare the Data API JSON with what AppSync returns to the client.
Steps:
- In the AppSync console, open your API → Settings → Logging.
- Choose (or create) a CloudWatch Log Group.
- Set Field resolver log level to at least ERROR (use INFO or ALL during development for full request/response logs).
- Save.
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.
- In the AppSync console, open Queries.
- Paste this query, replacing the airport name and distance as needed:
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
}
}
}
}- Click Run. You should see a JSON response with:
- An array of hotels within 50km of Les Loges airport
- Airport information including name and location coordinates
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.







