Cross-Project Configuration¶
Cross-project messaging allows a FastPubSub service to consume from or publish to Pub/Sub resources outside its default broker project. This model is common in multi-team organizations where platform events are shared across isolated Google Cloud projects.
Conceptual Model¶
FastPubSub starts with a default broker project_id.
You can override this default at subscriber, publisher, router, or per-call publish level.
flowchart LR
A[Service in project-a] --> B[Default broker project-a]
A --> C[Override to project-b]
A --> D[Override to shared-platform]
This hierarchy supports localized decisions without duplicating broker instances across modules.
Subscriber-Level Override¶
# This subscriber uses the default project (project-a)
@broker.subscriber(
alias="local-handler",
topic_name="local-events",
subscription_name="local-events-subscription",
)
async def handle_local_events(message: Message):
await process_local_event(message.data)
# This subscriber uses a different project (project-b)
@broker.subscriber(
alias="cross-project-handler",
topic_name="shared-events",
subscription_name="project-a-subscription",
project_id="project-b",
autocreate=True,
)
async def handle_cross_project_events(message: Message):
await process_shared_event(message.data)
Use this when only specific subscriptions belong to a foreign project.
Publisher-Level Override¶
# Publisher for the default project
local_publisher = broker.publisher("local-events")
# Publisher for a different project
cross_project_publisher = broker.publisher(
"shared-events", project_id="project-b"
)
@app.post("/send-event")
async def send_event(data: dict):
# Publish to local project
await local_publisher.publish(data)
# Publish to other project
await cross_project_publisher.publish(data)
You can also publish with broker-level API where project is specified per call:
@app.after_startup
async def publish_cross_project_broker() -> None:
await broker.publish(
topic_name="shared-events",
data={"event": "cross_project"},
project_id="other-project-id",
)
Router-Scoped Project Mapping¶
Routers are useful when a whole domain maps to the same remote project.
# Router for external project
external_router = PubSubRouter(prefix="external", project_id="project-b")
@external_router.subscriber(
alias="shared-handler",
topic_name="shared-events",
subscription_name="project-a-subscription",
)
async def handle_shared(message: Message):
await process_shared_event(message.data)
@external_router.subscriber(
alias="analytics-handler",
topic_name="analytics-events",
subscription_name="project-a-analytics-subscription",
)
async def handle_analytics(message: Message):
await process_analytics(message.data)
# Include the router in the broker
broker.include_router(external_router)
Nested Project Scopes¶
Nested routers can progressively refine scope:
# Create a separate broker for nested router demo
nested_broker = PubSubBroker(project_id="project-a")
# First level router - uses project-b
level1_router = PubSubRouter(prefix="external", project_id="project-b")
# Second level router - uses project-c
level2_router = PubSubRouter(prefix="analytics", project_id="project-c")
# Subscriber uses project-c (inherited from level2)
@level2_router.subscriber(
alias="handler",
topic_name="metrics",
subscription_name="metrics-subscription",
)
async def handle_metrics(message: Message):
pass
level1_router.include_router(level2_router)
nested_broker.include_router(level1_router)
This pattern is useful when organizational boundaries differ by event domain.
Composite Service Example¶
# Complete example: service consuming from multiple projects
complete_broker = PubSubBroker(project_id="my-service")
complete_app = FastPubSub(complete_broker)
# Local events
@complete_broker.subscriber(
alias="local-orders",
topic_name="orders",
subscription_name="orders-subscription",
)
async def handle_local_orders(message: Message):
await process_order(message.data)
# Events from shared platform
platform_router = PubSubRouter(prefix="platform", project_id="shared-platform")
@platform_router.subscriber(
alias="user-events",
topic_name="user-events",
subscription_name="my-service-user-subscription",
)
async def handle_user_events(message: Message):
await sync_user_data(message.data)
@platform_router.subscriber(
alias="notifications",
topic_name="notifications",
subscription_name="my-service-notifications-subscription",
)
async def handle_notifications(message: Message):
await send_notification(message.data)
complete_broker.include_router(platform_router)
# Publishing to both projects
@complete_app.post("/create-order")
async def create_order(order: dict):
# Local publish
await complete_broker.publish("orders", order)
# Notify platform
await complete_broker.publish(
"order-events",
{"order_id": order["id"], "action": "created"},
project_id="shared-platform",
)
The service consumes local and shared-platform events while publishing to both contexts.
IAM Requirements¶
Cross-project configuration is correct only when IAM allows it. The service account executing your consumer must have permissions in the target project.
Subscribe Permissions¶
gcloud projects add-iam-policy-binding project-b \
--member="serviceAccount:my-service@project-a.iam.gserviceaccount.com" \
--role="roles/pubsub.subscriber"
Publish Permissions¶
gcloud projects add-iam-policy-binding project-b \
--member="serviceAccount:my-service@project-a.iam.gserviceaccount.com" \
--role="roles/pubsub.publisher"
Use Least Privilege
Prefer topic/subscription scoped bindings where feasible. Project-wide roles are simpler initially but increase blast radius.
Validation with PubSubTestClient¶
For local checks, validate project routing decisions in published metadata.
async def test_cross_project_publish_targets_expected_project() -> None:
test_broker = PubSubBroker(project_id="project-a")
async with PubSubTestClient(test_broker) as client:
await client.publish(
topic="shared-events",
data={"event": "cross-project"},
project_id="project-b",
)
published = client.get_published_messages()
assert len(published) == 1
assert published[0].project_id == "project-b"
This verifies your FastPubSub configuration intent without external infrastructure.
Design Recommendations¶
Naming Convention¶
Include producer and consumer context in subscription names, for example:
project-a-orders-subscriptionmy-service-user-events-subscription
Descriptive names reduce ambiguity in audits and incident response.
Dependency Registry¶
Maintain a simple mapping document:
- Source project
- Target project
- Topic/subscription
- Owning team
- IAM principal
This materially improves change impact analysis.
Separate Platform and Product Flows¶
Where possible, group cross-project routes by router prefix (platform.*, external.*) to improve code discoverability.
Common Failure Modes¶
- Correct code with missing IAM grants in target project.
- Inconsistent naming for cross-project subscriptions.
- Mixing local and cross-project publish paths without explicit project override.
- Forgetting to document ownership boundaries across teams.
Recap¶
- FastPubSub supports cross-project routing at subscriber, publisher, router, and per-publish levels.
- IAM is a mandatory part of correctness for cross-project flows.
- Router-level scoping keeps multi-project architectures maintainable.
PubSubTestClienthelps validate routing intent in local tests.- Strong naming and ownership conventions are essential in shared environments.