| name | speckle-core-transport |
| description | Use when sending or receiving Speckle objects, configuring transports, or debugging data transfer issues. Prevents SQLite lock errors, auth token expiry during long transfers, and missing cache-first receive patterns. Covers ServerTransport, SQLiteTransport, MemoryTransport, DiskTransport, send/receive flow, caching strategy, and progress callbacks. Keywords: speckle transport, server transport, sqlite transport, send, receive, cache, operations, upload, download, how to send data, upload model, receive model, sync data.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Speckle Server 2.x/3.x, SpecklePy, Speckle.Sdk. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
speckle-core-transport
Quick Reference
Transport Comparison Table
| Transport | Storage | Persistent | Network | Use Case |
|---|
| ServerTransport | Speckle Server (HTTP) | Yes (remote) | Yes | Collaborative send/receive to shared projects |
| SQLiteTransport | Local SQLite database | Yes (local) | No | Default local cache, offline access |
| MemoryTransport | In-memory dictionary | No | No | Testing, temporary pipelines, benchmarking |
| DiskTransport | Individual files on disk | Yes (local) | No | Project-local storage, git-tracked objects (C# only) |
Decision Tree: Choosing the Right Transport
Need to share data with others?
├── YES → ServerTransport (+ SQLiteTransport as cache)
└── NO
├── Need persistence across restarts?
│ ├── YES
│ │ ├── Need git-trackable individual files? → DiskTransport (C# only)
│ │ └── Need fast bulk read/write? → SQLiteTransport
│ └── NO
│ ├── Testing or benchmarking? → MemoryTransport
│ └── Serverless / ephemeral environment? → MemoryTransport
Critical Warnings
NEVER call ServerTransport.get_object() in Python -- it is NOT implemented and raises SpeckleException. ALWAYS use operations.receive() or copy_object_and_children() instead.
NEVER call send() with use_default_cache=False and an empty transport list -- this raises SpeckleException because there is no destination for the data.
NEVER rely on MemoryTransport for persistent storage -- all data is lost when the process exits or the transport is garbage collected.
NEVER call get_all_objects() on a large SQLiteTransport -- this loads the entire database into memory and causes memory exhaustion on large caches.
NEVER create a ServerTransport without authentication -- all Speckle Server API calls require a valid token. Construction succeeds but every operation fails.
NEVER omit CancellationToken in C# interactive applications -- without it, long send/receive operations cannot be gracefully stopped.
NEVER forget to call end_write() after manual transport writes -- batched data may not be flushed. Use operations.send() instead, which manages the lifecycle automatically.
ALWAYS use the default cache (use_default_cache=True) during send unless you have a specific reason not to -- without it, every receive requires a full re-download from the server.
Transport Interface
All transports implement a common interface that decouples serialization from storage. The serializer writes to any transport without knowing the backend.
Python: AbstractTransport
All Python transports inherit from AbstractTransport (ABC). Core methods:
| Method | Purpose |
|---|
save_object(id, serialized_object) | Store one serialized object by hash |
get_object(id) -> Optional[str] | Retrieve by hash, None if not found |
has_objects(id_list) -> Dict[str, bool] | Bulk existence check |
begin_write() | Signal start of write batch |
end_write() | Flush pending writes |
save_object_from_transport(id, source) | Copy from another transport |
copy_object_and_children(id, target) | Copy object tree to target |
C#: ITransport
The C# interface adds async support, cancellation, progress reporting, and telemetry:
| Member | Purpose |
|---|
SaveObject(id, serializedObject) | Enqueue object for storage |
GetObject(id) -> Task<string?> | Async retrieval by hash |
HasObjects(objectIds) -> Task<Dict<string, bool>> | Async bulk existence check |
BeginWrite() / EndWrite() | Write lifecycle control |
WriteComplete() -> Task | Await completion of all queued writes |
CancellationToken | Graceful cancellation support |
OnProgressAction | IProgress<ProgressArgs> callback |
Elapsed | TimeSpan for performance measurement |
IBlobCapableTransport extends ITransport with BlobStorageFolder and SaveBlob(Blob) for binary file support.
ServerTransport
Communicates with a Speckle Server over HTTP. Primary transport for collaborative workflows.
Python Constructor
ServerTransport(
stream_id: str,
client: Optional[SpeckleClient] = None,
account: Optional[Account] = None,
token: Optional[str] = None,
url: Optional[str] = None,
name: str = "RemoteTransport",
)
You MUST provide exactly one authentication path:
client -- an already-authenticated SpeckleClient
account -- an Account object containing token + server URL
token + url -- raw credentials
C# Constructor
new ServerTransport(
ISpeckleHttp http,
ISdkActivityFactory activityFactory,
Account account,
string streamId,
int timeoutSeconds = 60,
string? blobStorageFolder = null
)
Uses dependency injection for ISpeckleHttp and ISdkActivityFactory. Authentication via Account.token.
Implementation Details
| Aspect | Python | C# |
|---|
| Batch upload size | 1 MB | Configurable via send buffer |
get_object() | NOT implemented (raises exception) | Async, functional |
| Deduplication | Defers to copy operations | Calls HasObjects() before upload |
| Background sending | No | Dedicated SendingThreadMain() thread |
| Blob support | No | Yes (IBlobCapableTransport) |
Server API Endpoints
| Endpoint | Purpose |
|---|
/objects/{stream_id}/{id}/single | Download single object |
/api/getobjects/{stream_id} | Batch download multiple objects |
SQLiteTransport
Persistent local storage using SQLite. Serves as the default local cache for all Speckle operations.
Constructors
Python:
SQLiteTransport(
base_path: str = None,
app_name: str = None,
scope: str = None,
max_batch_size_mb: float = None,
)
C#:
new SQLiteTransport(
string? basePath = null,
string? applicationName = null,
string? scope = null
)
Path construction: {basePath}/{applicationName}/{scope}.db
Default Data Paths Per OS
| OS | Default Path |
|---|
| Windows | %APPDATA%\Speckle (typically C:\Users\{user}\AppData\Roaming\Speckle) |
| macOS | ~/.config/Speckle |
| Linux | $XDG_DATA_HOME/Speckle or ~/.local/share/Speckle |
Default cache database: {speckle_dir}/Data.db (C#) or {speckle_dir}/Objects.db (Python).
Write Optimization
- Python: Accumulates objects in
_current_batch, flushes via executemany() with INSERT OR IGNORE when batch size exceeds max_batch_size_mb
- C#: Uses
ConcurrentQueue with a 500ms auto-flush timer, processes up to 1,000 items per transaction
MemoryTransport
Stores objects in an in-memory dictionary. Simplest transport.
Python: Dict[str, str] backing store. save_object_from_transport() and copy_object_and_children() are NOT implemented.
C#: ConcurrentDictionary<string, string> backing store. Full ITransport implementation including async methods.
Use cases: Unit testing, temporary pipelines, serverless functions, benchmarking serialization speed.
Limitation: No size limits -- large datasets consume proportional memory. All data lost on garbage collection.
DiskTransport (C# Only)
Stores each object as a separate file on the filesystem. Useful for project-local storage, git-tracked objects, network drive sharing, and human-readable debugging.
For project-local SQLite storage (better performance for large datasets), use SQLiteTransport with a custom basePath instead.
Send Flow
Python: operations.send()
send(
base: Base,
transports: Optional[List[AbstractTransport]] = None,
use_default_cache: bool = True,
) -> str
Flow:
- If
use_default_cache=True → prepend a SQLiteTransport to the transport list
- If
use_default_cache=False AND no transports → raise SpeckleException
- Create
BaseObjectSerializer with all transports
- Serialize root object recursively -- each finished object is immediately sent to ALL transports
- String representations are garbage collected after sending (memory optimization)
- Return root object hash
C#: Operations.Send()
async Task<(string rootObjId, IReadOnlyDictionary<string, ObjectReference> convertedReferences)> Send(
Base value,
IReadOnlyCollection<ITransport> transports,
IProgress<ProgressArgs>? onProgressAction = null,
CancellationToken cancellationToken = default
)
Flow:
- Validate:
value not null, transports not empty
- Configure each transport with progress action and cancellation token
- Call
BeginWrite() on all transports
- Serialize via
SpeckleObjectSerializer
- Call
EndWrite() on all transports (in finally block)
- Return tuple of (root hash, converted references dictionary)
Multi-transport: Objects are written to ALL provided transports simultaneously.
Receive Flow
Python: operations.receive()
receive(
obj_id: str,
remote_transport: Optional[AbstractTransport] = None,
local_transport: Optional[AbstractTransport] = None,
) -> Base
C#: Operations.Receive()
async Task<Base> Receive(
string objectId,
ITransport? remoteTransport = null,
ITransport? localTransport = null,
IProgress<ProgressArgs>? onProgressAction = null,
CancellationToken cancellationToken = default
)
Cache-First Strategy (Both SDKs)
- If
localTransport is None → create default SQLiteTransport
- Check local transport for the object
- If found locally → deserialize directly from cache
- If NOT found → call
copy_object_and_children() on remote transport:
- Download root object
- Check which children exist locally
- Download only missing children in batches
- Write all to local transport
- Deserialize from local transport
- Return reconstructed
Base object
The local transport is ALWAYS checked first. After a remote fetch, objects are cached locally for future use.
Caching Strategy
Content-Addressed Immutability
Speckle objects are content-addressed: every object's id is a SHA256 hash of its content. This means:
- An object with a given
id ALWAYS has the same content
- Cached data is NEVER stale -- objects are immutable
- New versions of conceptual objects (e.g., a modified wall) have DIFFERENT ids
- No cache invalidation logic is needed
Cache Growth
The cache grows monotonically -- there is no built-in garbage collection or LRU eviction. Users can manually delete the cache database (it is recreated on next use), but all previously cached objects must be re-downloaded.
Deduplication During Send (C#)
- Serialize all objects locally
- Call
HasObjects() on the server with the list of object ids
- Upload only objects the server does not already have
Sending the same data twice transmits only the delta. A model where 5% of objects changed uploads only that 5%.
Progress Reporting
C# -- IProgress
var progress = new Progress<ProgressArgs>(args =>
{
Console.WriteLine($"{args.ProgressEvent}: {args.Count}");
});
await Operations.Send(data, transports, onProgressAction: progress);
Python -- Transport Object Count
transport = ServerTransport(stream_id="...", client=client)
hash = send(my_object, transports=[transport])
Reference Links
Official Sources