Getting Started with Geospatial gRPC
August 20, 2026 · View on GitHub
This guide will help you quickly get up and running with the Geospatial gRPC protocols.
Prerequisites
- Buf CLI: Install Buf for protocol buffer management
- Development Environment: Your preferred language with gRPC support
- Basic gRPC Knowledge: Understanding of Protocol Buffers and gRPC concepts
Step 1: Install Tools
Install Buf CLI
# macOS
brew install bufbuild/buf/buf
# Linux/WSL
curl -sSL https://github.com/bufbuild/buf/releases/latest/download/buf-Linux-x86_64.tar.gz | tar -xzf - -C /usr/local buf/bin/buf
# Windows
# Download from: https://github.com/bufbuild/buf/releases
Verify Installation
buf --version
Step 2: Clone the Repository
git clone https://github.com/honua-io/geospatial-grpc.git
cd geospatial-grpc
The stable schema is also public on the BSR. To generate from its immutable release coordinate while using this checkout's generation template:
buf generate buf.build/honua-io/geospatial-grpc:v1.0.0
Step 3: Generate Client Libraries
Generate for All Languages
buf generate
This creates client libraries in the gen/ directory:
gen/
├── csharp/ # C# / .NET
├── go/ # Go
├── java/ # Java
├── python/ # Python
├── rust/ # Rust
├── swift/ # Swift
└── typescript/ # TypeScript/JavaScript
Generate for Specific Language
# Only C#
buf generate --template buf.gen.yaml --include-imports --path geospatial/v1 --output gen/csharp
# Only TypeScript
buf generate --template buf.gen.yaml --include-imports --path geospatial/v1 --output gen/typescript
Step 4: Set Up Your Development Environment
.NET / C#
- Create a new project:
dotnet new console -n GeospatialGrpcExample
cd GeospatialGrpcExample
- Add gRPC packages:
dotnet add package Grpc.Net.Client
dotnet add package Geospatial.Grpc --version 1.0.0
This restore uses nuget.org; no GitHub Packages credential or source checkout is required.
TypeScript / JavaScript
- Create a new Node.js project:
npm init -y
npm install @bufbuild/protobuf @connectrpc/connect @connectrpc/connect-node
- Copy generated files:
cp -r ../gen/typescript/src .
Python
- Set up virtual environment:
python -m venv venv
source venv/bin/activate # Linux/Mac
# or
venv\Scripts\activate # Windows
- Install gRPC packages:
pip install grpcio grpcio-tools
- Copy generated files:
cp -r ../gen/python/* .
Step 5: Your First Query
.NET Example
using Geospatial.V1;
using Grpc.Net.Client;
// Create gRPC channel
using var channel = GrpcChannel.ForAddress("https://api.example.com");
var client = new FeatureService.FeatureServiceClient(channel);
// Build query request
var request = new QueryFeaturesRequest
{
ServiceId = "parks",
LayerId = 0,
Where = "AREA > 1000",
ReturnGeometry = true,
ResultRecordCountLong = 100
};
// Execute query
var response = await client.QueryFeaturesAsync(request);
// Process results
Console.WriteLine($"Found {response.Features.Count} features");
foreach (var feature in response.Features)
{
Console.WriteLine($"Feature ID: {feature.Id}");
if (feature.Geometry?.Point != null)
{
var point = feature.Geometry.Point;
Console.WriteLine($"Location: {point.X}, {point.Y}");
}
}
TypeScript Example
import { FeatureService } from './gen/geospatial/v1/feature_service_pb';
import { createClient } from '@connectrpc/connect';
import { createGrpcTransport } from '@connectrpc/connect-node';
// Create transport and client
const transport = createGrpcTransport({
baseUrl: 'https://api.example.com'
});
const client = createClient(FeatureService, transport);
// Build and execute query
const response = await client.queryFeatures({
serviceId: 'parks',
layerId: 0,
where: 'AREA > 1000',
returnGeometry: true,
resultRecordCountLong: 100n
});
// Process results
console.log(`Found ${response.features.length} features`);
response.features.forEach(feature => {
console.log(`Feature ID: ${feature.id}`);
if (feature.geometry?.point) {
const { x, y } = feature.geometry.point;
console.log(`Location: ${x}, ${y}`);
}
});
Python Example
import grpc
from geospatial.v1 import feature_service_pb2
from geospatial.v1 import feature_service_pb2_grpc
# Create gRPC channel and client
channel = grpc.insecure_channel('api.example.com:443')
client = feature_service_pb2_grpc.FeatureServiceStub(channel)
# Build query request
request = feature_service_pb2.QueryFeaturesRequest(
service_id='parks',
layer_id=0,
where='AREA > 1000',
return_geometry=True,
result_record_count_long=100
)
# Execute query
response = client.QueryFeatures(request)
# Process results
print(f'Found {len(response.features)} features')
for feature in response.features:
print(f'Feature ID: {feature.id}')
if feature.geometry.HasField('point'):
point = feature.geometry.point
print(f'Location: {point.x}, {point.y}')
Step 6: Working with Forms
Get Form Definition
using Geospatial.V1;
var formClient = new FormService.FormServiceClient(channel);
var formRequest = new GetFormDefinitionRequest
{
FormId = "park-inspection",
ServiceId = "parks",
LayerId = 0,
MobileCapabilities = new MobileCapabilities
{
HasCamera = true,
HasGps = true,
Platform = "ios",
DeviceType = "phone",
NetworkType = NetworkType.Wifi
}
};
var formResponse = await formClient.GetFormDefinitionAsync(formRequest);
var form = formResponse.Form;
Console.WriteLine($"Form: {form.Title}");
foreach (var control in form.Controls)
{
Console.WriteLine($" Field: {control.Label}");
}
Submit Form Data
var submission = new SubmitFormDataRequest
{
FormId = "park-inspection",
FormVersion = "1.0",
Instance = new FormInstance
{
InstanceId = Guid.NewGuid().ToString(),
FormId = "park-inspection",
CreatedBy = "user123",
Status = InstanceStatus.Complete
}
};
// Add field values
submission.Instance.FieldValues["inspector_name"] = new AttributeValue
{
StringValue = "John Doe"
};
submission.Instance.FieldValues["inspection_date"] = new AttributeValue
{
DatetimeValue = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
submission.Instance.FieldValues["location"] = new AttributeValue
{
StringValue = "POINT(-122.4194 37.7749)" // San Francisco
};
var submitResponse = await formClient.SubmitFormDataAsync(submission);
if (submitResponse.Result.Success)
{
Console.WriteLine($"Created feature ID: {submitResponse.CreatedFeatureId}");
}
Step 7: Streaming Large Datasets
For large queries, use streaming to avoid memory issues:
var streamRequest = new QueryFeaturesRequest
{
ServiceId = "parcels",
LayerId = 0,
Where = "1=1", // All features
ReturnGeometry = false, // Attributes only for performance
ResultRecordCountLong = 1000 // Page size
};
using var streamCall = client.QueryFeaturesStream(streamRequest);
var totalCount = 0;
await foreach (var page in streamCall.ResponseStream.ReadAllAsync())
{
totalCount += page.Features.Count;
Console.WriteLine($"Processed {page.Features.Count} features (total: {totalCount})");
// Process features in this page
foreach (var feature in page.Features)
{
// Process individual feature
Console.WriteLine($"Feature {feature.Id}");
}
if (page.IsLastPage)
{
break;
}
}
Console.WriteLine($"Total features processed: {totalCount}");
Step 8: Error Handling
Always implement proper error handling:
try
{
var response = await client.QueryFeaturesAsync(request);
// Process response
}
catch (RpcException ex)
{
switch (ex.StatusCode)
{
case StatusCode.NotFound:
Console.WriteLine("Service or layer not found");
break;
case StatusCode.InvalidArgument:
Console.WriteLine($"Invalid request: {ex.Status.Detail}");
break;
case StatusCode.PermissionDenied:
Console.WriteLine("Access denied");
break;
default:
Console.WriteLine($"gRPC error: {ex.Status}");
break;
}
}
Step 9: Configuration for Production
Connection Configuration
var channel = GrpcChannel.ForAddress("https://api.production.com", new GrpcChannelOptions
{
// Connection settings
MaxReceiveMessageSize = 16 * 1024 * 1024, // 16MB
MaxSendMessageSize = 4 * 1024 * 1024, // 4MB
// Retry configuration
//
// IMPORTANT: read-only (idempotent) methods are always safe to retry.
// Mutating RPCs are only safe to retry when the request carries a stable
// idempotency_key: the server MUST treat a retry that reuses the same key
// within its dedup window as the same operation and return the original
// result, so a retry after a lost response cannot SILENTLY DUPLICATE the
// write. The protocol defines this idempotency_key field on the mutating
// RPCs — FeatureService.ApplyEdits, FormService.SubmitFormData, and the
// operator Submit*/Execute*/RollbackDeployment requests. Set a stable key
// (e.g. a UUID per logical attempt) before adding those methods to a retry
// policy. Do NOT use MethodName.Default to blanket-retry every method,
// because that would also retry mutations on calls where you did not set an
// idempotency_key, which can duplicate the write. Instead, scope the policy
// to specific read-only methods (below) plus any mutating method for which
// you always send an idempotency_key.
ServiceConfig = new ServiceConfig
{
MethodConfigs =
{
new MethodConfig
{
Names =
{
new MethodName { Service = "geospatial.v1.FeatureService", Method = "QueryFeatures" },
new MethodName { Service = "geospatial.v1.FeatureService", Method = "QueryFeaturesStream" },
new MethodName { Service = "geospatial.v1.FormService", Method = "GetFormDefinition" },
new MethodName { Service = "geospatial.v1.FormService", Method = "GetFormMetadata" },
new MethodName { Service = "geospatial.v1.FormService", Method = "ValidateFormData" },
},
RetryPolicy = new RetryPolicy
{
MaxAttempts = 3,
InitialBackoff = TimeSpan.FromSeconds(1),
MaxBackoff = TimeSpan.FromSeconds(5),
BackoffMultiplier = 1.5,
RetryableStatusCodes = { StatusCode.Unavailable }
}
}
}
}
});
Authentication
// API Key authentication
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
metadata.Add("Authorization", "Bearer your-api-key-here");
return Task.CompletedTask;
});
var channel = GrpcChannel.ForAddress("https://api.production.com", new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(new SslCredentials(), credentials)
});
Step 10: Workspace, Artifact, Process, Pipeline, Render, Build, and Deployment Workflows
The protocol includes seven services for server-side workflows:
WorkspaceService— create, open, get, list, update, promote, retain, and release workspaces; inspect quota. Owns the canonicalWorkspaceRefhandle used by every operator service.ArtifactService— publish (client-stream), read (server-stream), get, inspect, list, retain, and release artifacts; resolve retention policies. Owns the canonicalArtifactRefhandle and theArtifactresource.ProcessService— validate, dry-run, and execute geospatial analysis plans (synchronous, streaming, or async job)PipelineService— validate, dry-run, and execute data publishing pipelines with stage-by-stage progressRenderService— compose maps and produce a canonicalMapPackagewith streaming progressBuilderService— synthesize anAppPackagefrom templates, intent, and data bindingsDeploymentService— promoteAppPackage/MapPackage/ArtifactRefto live targets with rollback and health telemetry
The operator execution services (ProcessService, PipelineService, RenderService, BuilderService, and DeploymentService) share execution infrastructure defined in execution_types.proto (job lifecycle states, structured errors with retryability guidance, artifact references, provenance records) and follow the same Validate* / DryRun* / Execute* / Execute*Stream / Submit*Job / Get*Job / Get*JobResult / Cancel*Job RPC surface. WorkspaceService and ArtifactService are lifecycle services with their own RPC shape — WorkspaceService exposes create/open/get/list/update/promote/retain/release/quota RPCs and ArtifactService exposes publish/read/get/inspect/list/retain/release plus retention-policy reads. Workspace and artifact lifecycle contracts live in workspace_artifact_types.proto, workspace_service.proto, and artifact_service.proto. Canonical packaging shapes (MapPackage, AppPackage, DeploymentSpec) live in packaging_types.proto. The sections below start with workspace and artifact lifecycle flows, then use ProcessService to illustrate the shared execution pattern; render, build, and deploy follow the same execution shape with their own spec types (RenderSpec, BuildSpec, DeploymentSpec).
Create and Open a Workspace
var workspaceClient = new WorkspaceService.WorkspaceServiceClient(channel);
var created = await workspaceClient.CreateWorkspaceAsync(
new CreateWorkspaceRequest
{
Desired = new Workspace
{
Quota = new QuotaSpec
{
MaxBytes = 10L * 1024 * 1024 * 1024,
MaxArtifacts = 5000
},
DefaultRetention = new RetentionPolicyRef { RetentionPolicyId = "retain-30d" },
Labels = { ["team"] = "analytics" },
Metadata = { ["purpose"] = "render-build-deploy" }
},
Context = new ExecutionContext
{
Metadata = { ["caller"] = "getting-started" }
}
});
var workspaceRef = created.Workspace.Ref;
var opened = await workspaceClient.OpenWorkspaceAsync(
new OpenWorkspaceRequest
{
Ref = workspaceRef,
Context = new ExecutionContext
{
Metadata = { ["caller"] = "getting-started" }
}
});
Console.WriteLine(
$"Workspace {opened.Workspace.Ref.WorkspaceId} @ {opened.Workspace.Ref.WorkspaceRevision}");
WorkspaceRef is the canonical handle. When a request already carries Ref, that handle is authoritative; populate ExecutionContext.Workspace only when you need it elsewhere and keep it identical. CreateWorkspace may seed quota, default_retention, labels, and metadata; UpdateWorkspace rewrites quota, labels, and metadata only. Promotion, retention, release, and observed expiry stay on the lifecycle RPCs.
Publish, Inspect, and List Artifacts
using Google.Protobuf;
var artifactClient = new ArtifactService.ArtifactServiceClient(channel);
using var publish = artifactClient.PublishArtifact();
await publish.RequestStream.WriteAsync(new PublishArtifactRequest
{
Header = new ArtifactHeader
{
Workspace = workspaceRef,
ArtifactClass = ArtifactClass.File,
ContentType = "application/json",
Retention = new RetentionPolicyRef { RetentionPolicyId = "retain-30d" },
ProducerRef = "render-job-42"
}
});
var payload = ByteString.CopyFromUtf8("{\"status\":\"ready\"}");
await publish.RequestStream.WriteAsync(new PublishArtifactRequest
{
Chunk = new ArtifactChunk
{
Data = payload,
Offset = 0,
Last = true
}
});
await publish.RequestStream.CompleteAsync();
var published = await publish.ResponseAsync;
if (published.OutcomeCase != PublishArtifactResponse.OutcomeOneofCase.Result)
{
Console.WriteLine($"Publish failed: {published.Error.Message}");
return;
}
var artifact = published.Result;
var inspection = await artifactClient.InspectArtifactAsync(
new InspectArtifactRequest { Ref = artifact.Ref });
var page = await artifactClient.ListArtifactsAsync(
new ListArtifactsRequest
{
Workspace = artifact.Ref.Workspace,
ClassFilter = artifact.Ref.ArtifactClass
});
Console.WriteLine(
$"{inspection.MaterializationState}: {page.Artifacts.Count} matching artifact(s)");
PublishArtifact requires an ArtifactHeader as the first stream message; every later message is an ArtifactChunk. ArtifactHeader.Workspace is the authoritative workspace selector for upload. Prefer InspectArtifact when you need materialization state, observed size, or hash without paying to stream bytes, and use ReadArtifact with offset_bytes / max_bytes only when you need the body. ListArtifactsRequest.Workspace is optional: unset lists artifacts across every workspace visible to the caller; when set it is authoritative over ExecutionContext.Workspace. RetainArtifact and ReleaseArtifact stay unary but return the same in-band Artifact | ErrorDetail outcome shape used by PublishArtifact.
Validate and Dry-Run a Plan
.NET
using Geospatial.V1;
var processClient = new ProcessService.ProcessServiceClient(channel);
// Build a plan with typed steps
var plan = new ExecutionPlan
{
PlanId = "buffer-analysis-1",
SpecVersion = "1",
WorkflowFamily = WorkflowFamily.Analyze
};
plan.Steps.Add(new PlanStep
{
StepId = "query",
Kind = "query_features",
Inputs =
{
["service_id"] = new ParameterValue { StringValue = "parcels" },
["where"] = new ParameterValue { StringValue = "ZONE = 'R1'" },
["spatial_filter"] = new ParameterValue
{
SpatialFilterValue = new SpatialFilter
{
Geometry = new Geometry
{
Polygon = new PolygonGeometry
{
Rings = { /* ring coordinates */ }
}
},
SpatialRelationship = SpatialRelationship.Intersects
}
}
}
});
plan.Steps.Add(new PlanStep
{
StepId = "buffer",
Kind = "geoprocess",
Inputs =
{
["operation"] = new ParameterValue { StringValue = "buffer" },
["distance"] = new ParameterValue { DoubleValue = 100.0 }
},
Dependencies = { "query" }
});
// Validate the plan
var validation = await processClient.ValidatePlanAsync(
new ValidatePlanRequest { Plan = plan });
if (!validation.Valid)
{
foreach (var issue in validation.Issues)
Console.WriteLine($"[{issue.Severity}] {issue.NodeId}: {issue.Message}");
return;
}
// Estimate cost before executing
var dryRun = await processClient.DryRunPlanAsync(
new DryRunPlanRequest { Plan = plan });
Console.WriteLine($"Estimated duration: {dryRun.Result.EstimatedDurationSeconds}s");
foreach (var artifact in dryRun.Result.EstimatedArtifacts)
Console.WriteLine($" {artifact.ArtifactClass}: ~{artifact.EstimatedSizeBytes} bytes");
TypeScript
import { ProcessService } from './gen/geospatial/v1/process_service_pb.js';
import { createClient } from '@connectrpc/connect';
const processClient = createClient(ProcessService, transport);
const plan = {
planId: 'buffer-analysis-1',
specVersion: '1',
workflowFamily: 1, // WORKFLOW_FAMILY_ANALYZE
steps: [
{
stepId: 'query',
kind: 'query_features',
inputs: {
service_id: { kind: { case: 'stringValue', value: 'parcels' } },
where: { kind: { case: 'stringValue', value: "ZONE = 'R1'" } }
}
},
{
stepId: 'buffer',
kind: 'geoprocess',
inputs: {
operation: { kind: { case: 'stringValue', value: 'buffer' } },
distance: { kind: { case: 'doubleValue', value: 100.0 } }
},
dependencies: ['query']
}
]
};
const validation = await processClient.validatePlan({ plan });
if (!validation.valid) {
validation.issues.forEach(issue =>
console.log(`[${issue.severity}] ${issue.nodeId}: ${issue.message}`)
);
} else {
const dryRun = await processClient.dryRunPlan({ plan });
console.log(`Estimated duration: ${dryRun.result?.estimatedDurationSeconds}s`);
}
Python
from geospatial.v1 import process_service_pb2
from geospatial.v1 import process_service_pb2_grpc
from geospatial.v1 import execution_types_pb2
process_stub = process_service_pb2_grpc.ProcessServiceStub(channel)
plan = execution_types_pb2.ExecutionPlan(
plan_id='buffer-analysis-1',
spec_version='1',
workflow_family=execution_types_pb2.WORKFLOW_FAMILY_ANALYZE,
steps=[
execution_types_pb2.PlanStep(
step_id='query',
kind='query_features',
inputs={
'service_id': execution_types_pb2.ParameterValue(string_value='parcels'),
'where': execution_types_pb2.ParameterValue(string_value="ZONE = 'R1'"),
}
),
execution_types_pb2.PlanStep(
step_id='buffer',
kind='geoprocess',
inputs={
'operation': execution_types_pb2.ParameterValue(string_value='buffer'),
'distance': execution_types_pb2.ParameterValue(double_value=100.0),
},
dependencies=['query']
),
]
)
validation = process_stub.ValidatePlan(
process_service_pb2.ValidatePlanRequest(plan=plan))
if not validation.valid:
for issue in validation.issues:
print(f'[{issue.severity}] {issue.node_id}: {issue.message}')
else:
dry_run = process_stub.DryRunPlan(
process_service_pb2.DryRunPlanRequest(plan=plan))
print(f'Estimated duration: {dry_run.result.estimated_duration_seconds}s')
Execute Synchronously
var response = await processClient.ExecutePlanAsync(
new ExecutePlanRequest
{
Plan = plan,
Context = new ExecutionContext
{
Workspace = new WorkspaceRef { WorkspaceId = "ws-1" }
}
});
switch (response.OutcomeCase)
{
case ExecutePlanResponse.OutcomeOneofCase.Result:
Console.WriteLine($"Completed: {response.Result.Summary}");
foreach (var artifact in response.Result.Artifacts)
Console.WriteLine($" Artifact: {artifact.ArtifactId} ({artifact.ArtifactClass})");
break;
case ExecutePlanResponse.OutcomeOneofCase.Error:
Console.WriteLine($"Failed [{response.Error.Category}]: {response.Error.Message}");
Console.WriteLine($" Retryability: {response.Error.Retryability}");
break;
}
See the Protocol Specification for the full RPC surface, streaming execution, async job management, pipeline definitions, and the render/build/deployment contracts (including MapPackage, AppPackage, DeploymentSpec, rollback, and deployment health telemetry).
Next Steps
- Explore Examples: Check the
examples/directory for complete projects - Read the Specification: Understand the protocol details in
docs/specification.md - Join the Community: Ask questions in GitHub Discussions
- Build Something Cool: Use the protocols in your own projects!
Common Issues and Solutions
Buf Generate Fails
# Clear module cache and regenerate
buf mod clear-cache
buf generate
Import Errors
Make sure you've correctly installed the generated files in your project and imported the required gRPC packages.
Connection Issues
Verify the server endpoint and ensure your client can reach it:
# Test basic connectivity
curl -v https://api.example.com/health
SSL/TLS Issues
For development, you may need to disable SSL verification:
var channel = GrpcChannel.ForAddress("http://localhost:5000", new GrpcChannelOptions
{
Credentials = ChannelCredentials.Insecure
});
Support
- GitHub Issues: Report bugs and request features
- Discussions: Ask questions and share ideas
- Email: geospatial-grpc@honua.io
- Documentation: Full protocol specification available