This week’s discussions around AI sovereignty and open source reminded us of a core principle: control over your data and tools is not just a philosophical stance - it’s a technical necessity. As we continue to push for open, self-hosted systems, we found ourselves building a Python client that interacts with a self-hosted instance of Postiz, a Bluesky-compatible social network. Here’s how we made it work.
The Problem: Self-Hosting and API Inconsistencies
We wanted to run a fully self-hosted social network, with full control over data, infrastructure, and the user experience. Postiz, being a self-hosted alternative to Bluesky, fit the bill. However, the API it exposes is not always consistent - specifically, when fetching posts via /api/public/v1/posts, the response can be either a single object or a list, depending on the query parameters. This inconsistency required careful handling on our end.
Our goal was to build a Python client that could reliably interact with a Postiz instance running in Docker Compose, with minimal boilerplate and clear error handling. We also needed to manage authentication headers and parse the response structure dynamically.
Our Approach: Docker Compose + Python Client
We deployed Postiz using Docker Compose, which made it easy to spin up a local instance for development and testing. Here’s a simplified version of our docker-compose.yml:
version: '3'
services:
postiz:
image: postiz/postiz:latest
ports:
- "8080:8080"
environment:
- POSTIZ_DB_URL=postgres://user:pass@db:5432/postiz
depends_on:
- db
db:
image: postgres:15
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- POSTGRES_DB=postiz
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
With Postiz running locally, we built a Python client that sends requests to /api/public/v1/posts. The client handles authentication via an Authorization header, which Postiz expects in the format Bearer <token>. The token is obtained via the /api/auth/token endpoint, which we also implemented in our code.
Code: Handling API Responses Dynamically
Here’s a snippet from our Python client, which demonstrates how we handle the API’s inconsistent response structure:
import requests
def fetch_posts(postiz_url, auth_token):
headers = {
"Authorization": f"Bearer {auth_token}"
}
response = requests.get(f"{postiz_url}/api/public/v1/posts", headers=headers)
response.raise_for_status()
data = response.json()
# Handle both single object and list responses
if isinstance(data, dict) and "items" in data:
return data["items"]
elif isinstance(data, list):
return data
else:
raise ValueError("Unexpected response structure from Postiz API")
This code ensures that regardless of whether the API returns a single item or a list, we can consistently work with a list of posts in our application. We also made sure to raise clear errors when the response structure is unexpected, which helps in debugging and maintaining the client over time.
Tradeoffs and Considerations
While this approach is simple and effective, it does come with tradeoffs. The dynamic handling of API responses adds a layer of complexity that could be avoided with a more consistent API. Additionally, managing authentication tokens and ensuring secure storage is a non-trivial task, especially in production environments.
We also found that the Postiz API lacks some features that we would expect from a modern social network, such as pagination and rate limiting support. These are things we plan to address in our own middleware layer.
What’s Next
We’re currently working on extending our client to support more endpoints, including user relationships and media uploads. We’re also exploring ways to integrate this with our on-device AI infrastructure, enabling local processing of social media data without relying on cloud services. How would you approach extending this client for a production environment? We’re eager to hear your thoughts.
Top comments (0)