================================================================================ QUILIBRIUM QSTORAGE — STATIC SITE DEPLOYMENT (llms.txt) ================================================================================ ================================================================================ OVERVIEW ================================================================================ Quilibrium QStorage is S3-compatible object storage provided by Quilibrium at: Endpoint: https://qstorage.quilibrium.com It hosts static websites (HTML, CSS, JS, assets) behind public URLs. The API is standard S3, but three protocol-level incompatibilities mean deployments done with AWS CLI or naive boto3 will silently fail. This document contains everything needed to deploy a working static site. Site URL pattern (public access): https://BUCKET_NAME.qstorage.quilibrium.com/ ================================================================================ PREREQUISITES ================================================================================ - Quilibrium account at https://console.quilibrium.com Signup is manual. Automated account creation is not yet possible. - API credentials from QConsole: Services > QStorage > Generate API keys Returns: Access Key ID, Secret Access Key, Account ID - Python 3 with boto3: pip install boto3 AWS CLI v2 cannot be used for uploads (see GOTCHAS). - A local directory containing static site files (index.html, CSS, JS, etc.) - Account must be approved. New accounts may see AccessDenied or AccountSuspended until support can process the approval (until a technical backend fix is successfully rolled out). ================================================================================ CRITICAL GOTCHAS — READ BEFORE DEPLOYING ================================================================================ These three issues will cause silent failures. Every other mistake produces a clear error. These three do not. GOTCHA 1: AWS CLI v2 DOES NOT WORK FOR UPLOADS AWS CLI v2 uses aws-chunked transfer encoding and appends CRC64NVME checksums to requests. QStorage rejects these with AccessDenied. This is a protocol incompatibility, not a credentials problem. SOLUTION: Use Python with boto3 for ALL upload operations. AWS CLI can be used for read-only tasks (listing buckets, inspecting policies) but never for put-object or sync. GOTCHA 2: ACL IS STRIPPED DURING UPLOAD Passing ACL="public-read" inside upload_file()'s ExtraArgs is silently ignored by QStorage. The file uploads successfully but remains private. Anonymous visitors get 403 Forbidden. SOLUTION: The public-read ACL MUST be applied as a separate put_object_acl() call AFTER the upload completes. One call per object. GOTCHA 3: PATH-STYLE URLS REQUIRE AUTHENTICATION https://qstorage.quilibrium.com/BUCKET_NAME/index.html This URL style returns 403 for anonymous requests. SOLUTION: Use virtual-hosted-style URLs for public access: https://BUCKET_NAME.qstorage.quilibrium.com/index.html ================================================================================ STEP-BY-STEP WORKFLOW ================================================================================ Each step below includes working Python/boto3 code. Replace placeholder variables before running. Placeholders used throughout: YOUR_ACCESS_KEY_ID — from QConsole YOUR_SECRET_ACCESS_KEY — from QConsole YOUR_BUCKET_NAME — globally unique name you choose ./your-site-directory — local path to your static files Step 1: Create the S3 client ----------------------------- import boto3 s3 = boto3.client( "s3", endpoint_url="https://qstorage.quilibrium.com", aws_access_key_id="YOUR_ACCESS_KEY_ID", aws_secret_access_key="YOUR_SECRET_ACCESS_KEY", ) Step 2: Create a bucket ------------------------ BUCKET_NAME = "YOUR_BUCKET_NAME" try: s3.create_bucket(Bucket=BUCKET_NAME) print(f"Bucket created: {BUCKET_NAME}") except s3.exceptions.BucketAlreadyOwnedByYou: print(f"Bucket already exists: {BUCKET_NAME}") except Exception as e: if "BucketAlreadyExists" in str(e): print(f"Bucket name taken. Choose another.") else: raise Bucket names must be globally unique across all QStorage users. Step 3: Upload files with correct MIME types --------------------------------------------- import os import mimetypes SITE_DIR = "./your-site-directory" for root, dirs, files in os.walk(SITE_DIR): for filename in files: filepath = os.path.join(root, filename) key = os.path.relpath(filepath, SITE_DIR) content_type, _ = mimetypes.guess_type(filename) if content_type is None: content_type = "application/octet-stream" s3.upload_file( filepath, BUCKET_NAME, key, ExtraArgs={"ContentType": content_type}, ) print(f"Uploaded: {key} ({content_type})") ContentType must be set per object or browsers will download files instead of rendering them. Step 4: Apply public-read ACL (MANDATORY, separate step) --------------------------------------------------------- This is the step most people skip. Without it, files are uploaded but return 403 to anonymous visitors. See GOTCHA 2. for root, dirs, files in os.walk(SITE_DIR): for filename in files: filepath = os.path.join(root, filename) key = os.path.relpath(filepath, SITE_DIR) s3.put_object_acl( Bucket=BUCKET_NAME, Key=key, ACL="public-read", ) print(f"Public ACL applied: {key}") In practice, combine this with Step 3 — upload each file, then immediately apply its ACL before moving to the next file. Step 5: Enable static website hosting (optional) -------------------------------------------------- s3.put_bucket_website( Bucket=BUCKET_NAME, WebsiteConfiguration={ "IndexDocument": {"Suffix": "index.html"}, "ErrorDocument": {"Key": "index.html"}, # for SPA routing }, ) print("Website hosting enabled.") Step 6: Set bucket policy for public access (recommended) ---------------------------------------------------------- import json policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": f"arn:aws:s3:::{BUCKET_NAME}/*", } ], } s3.put_bucket_policy(Bucket=BUCKET_NAME, Policy=json.dumps(policy)) print("Bucket policy applied.") Step 7: Access the deployed site --------------------------------- With website hosting enabled: https://YOUR_BUCKET_NAME.qstorage.quilibrium.com/ Without website hosting: https://YOUR_BUCKET_NAME.qstorage.quilibrium.com/index.html DO NOT use path-style URLs — they require authentication: https://qstorage.quilibrium.com/YOUR_BUCKET_NAME/index.html (BROKEN) ================================================================================ FULL deploy.py SCRIPT ================================================================================ Copy, fill in the six configuration values, and run with: python3 deploy.py #!/usr/bin/env python3 """ Deploy a static site to Quilibrium QStorage. Requirements: pip install boto3 """ import json import mimetypes import os import sys import boto3 # -- Configuration --------------------------------------------------------- ACCESS_KEY_ID = "YOUR_ACCESS_KEY_ID" SECRET_ACCESS_KEY = "YOUR_SECRET_ACCESS_KEY" ENDPOINT_URL = "https://qstorage.quilibrium.com" BUCKET_NAME = "YOUR_BUCKET_NAME" SITE_DIR = "./your-site-directory" INDEX_DOCUMENT = "index.html" # -------------------------------------------------------------------------- def create_client(): return boto3.client( "s3", endpoint_url=ENDPOINT_URL, aws_access_key_id=ACCESS_KEY_ID, aws_secret_access_key=SECRET_ACCESS_KEY, ) def create_bucket(s3): try: s3.create_bucket(Bucket=BUCKET_NAME) print(f"Bucket created: {BUCKET_NAME}") except s3.exceptions.BucketAlreadyOwnedByYou: print(f"Bucket already exists: {BUCKET_NAME}") except Exception as e: if "BucketAlreadyExists" in str(e): print(f"Bucket name taken: {BUCKET_NAME}. Choose another.") sys.exit(1) raise def upload_files(s3): for root, dirs, files in os.walk(SITE_DIR): for filename in files: filepath = os.path.join(root, filename) key = os.path.relpath(filepath, SITE_DIR) content_type, _ = mimetypes.guess_type(filename) if content_type is None: content_type = "application/octet-stream" # Upload s3.upload_file( filepath, BUCKET_NAME, key, ExtraArgs={"ContentType": content_type}, ) # Apply public-read ACL as a SEPARATE call (mandatory) s3.put_object_acl( Bucket=BUCKET_NAME, Key=key, ACL="public-read", ) print(f" {key} ({content_type})") def enable_website_hosting(s3): s3.put_bucket_website( Bucket=BUCKET_NAME, WebsiteConfiguration={ "IndexDocument": {"Suffix": INDEX_DOCUMENT}, "ErrorDocument": {"Key": INDEX_DOCUMENT}, }, ) print("Static website hosting enabled.") def set_bucket_policy(s3): policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": f"arn:aws:s3:::{BUCKET_NAME}/*", } ], } s3.put_bucket_policy(Bucket=BUCKET_NAME, Policy=json.dumps(policy)) print("Public-read bucket policy applied.") def main(): s3 = create_client() create_bucket(s3) print("Uploading files...") upload_files(s3) enable_website_hosting(s3) set_bucket_policy(s3) print() print(f"Site URL: https://{BUCKET_NAME}.qstorage.quilibrium.com/") if __name__ == "__main__": main() ================================================================================ ERROR RECOVERY ================================================================================ SYMPTOM CAUSE FIX ----------------------------------- --------------------------- ------------------------------ AccessDenied on uploaded files ACL not applied separately Run put_object_acl() on each (GOTCHA 2) object as a separate call AccessDenied from AWS CLI upload AWS CLI v2 protocol issue Switch to boto3 for uploads (GOTCHA 1) (GOTCHA 1) AccountSuspended on any request Account pending approval Wait and retry. Contact Quilibrium support if it persists beyond 24 hours. BucketAlreadyExists Bucket name is taken Choose a different globally unique bucket name Files download instead of rendering Missing ContentType metadata Re-upload with ExtraArgs={"ContentType": "text/html"} or correct MIME 403 on public URL Using path-style URL Switch to virtual-hosted style: (GOTCHA 3) BUCKET_NAME.qstorage.quilibrium.com ================================================================================ CONFIGURATION REFERENCE ================================================================================ Endpoint URL: https://qstorage.quilibrium.com Region: us-east-1 (placeholder; does not affect routing) Console: https://console.quilibrium.com Credential path: Services > QStorage > Generate API keys Credential format (for ~/.aws/credentials if using AWS CLI for reads): [quilibrium] aws_access_key_id = YOUR_ACCESS_KEY_ID aws_secret_access_key = YOUR_SECRET_ACCESS_KEY Config format (for ~/.aws/config): [profile quilibrium] region = us-east-1 output = json URL patterns: Virtual-hosted (public, works): https://BUCKET_NAME.qstorage.quilibrium.com/PATH Path-style (auth required): https://qstorage.quilibrium.com/BUCKET_NAME/PATH Website root (if hosting on): https://BUCKET_NAME.qstorage.quilibrium.com/ ================================================================================ LIMITATIONS AND NOTES ================================================================================ - Account creation is manual only. Automated signup is not supported. There is no API for account registration. - Account approval may not be immediate. New accounts can receive AccountSuspended or AccessDenied errors until approved. Contact Quilibrium support channels if approval takes more than 24 hours. - MPCTLS (Multi-Party Computation Transport Layer Security) is listed as a future feature. It is not yet available and should not be referenced in deployment scripts. - Region configuration has no effect. QStorage uses a single endpoint. Use us-east-1 as a placeholder in all configs. - There is no CDN customization, cache control, or edge routing configuration available through the S3 API at this time. - Bucket names must be globally unique across all QStorage users, not just within your account. - The public-read ACL must be set per-object via put_object_acl(). Bucket policies complement but do not replace per-object ACLs on QStorage. - AWS CLI v2 cannot upload objects. Use boto3 exclusively for writes. AWS CLI can list buckets and inspect policies if needed.