{"id":35611298,"url":"https://github.com/functionland/fula-api","last_synced_at":"2026-05-24T02:01:00.234Z","repository":{"id":328909092,"uuid":"1111817927","full_name":"functionland/fula-api","owner":"functionland","description":"the S3-compatible API to store on Fula network at https://s3.cloud.fx.land","archived":false,"fork":false,"pushed_at":"2026-05-19T02:07:21.000Z","size":297505,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-19T03:50:20.515Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://docs.fx.land/fula-api/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/functionland.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":"AUDIT-REPORT-2026-04.md","citation":null,"codeowners":null,"security":"docs/SECURITY_AUDIT_ENCRYPTION_2026_03.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-07T17:32:19.000Z","updated_at":"2026-05-19T02:06:57.000Z","dependencies_parsed_at":null,"dependency_job_id":"1b045fe6-b551-405d-981a-8dd4d45f0ff1","html_url":"https://github.com/functionland/fula-api","commit_stats":null,"previous_names":["functionland/fula-api"],"tags_count":55,"template":false,"template_full_name":null,"purl":"pkg:github/functionland/fula-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2Ffula-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2Ffula-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2Ffula-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2Ffula-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/functionland","download_url":"https://codeload.github.com/functionland/fula-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/functionland%2Ffula-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33418550,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T22:14:44.296Z","status":"online","status_checked_at":"2026-05-24T02:00:06.296Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-05T04:06:31.484Z","updated_at":"2026-05-24T02:01:00.228Z","avatar_url":"https://github.com/functionland.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fula Storage API\n\n**S3-Compatible Decentralized Storage Engine powered by IPFS**\n\n[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE)\n[![Rust](https://img.shields.io/badge/rust-1.83+-orange.svg)](https://www.rust-lang.org)\n\n## Overview\n\nFula Storage provides an Amazon S3-compatible API backed by a decentralized network of IPFS nodes. It enables developers to build applications using familiar S3 tools and SDKs while benefiting from:\n\n- **🌐 Decentralization**: Data is stored across a network of individually owned IPFS nodes\n- **🔒 End-to-End Encryption**: Client-side AEAD (AES-256-GCM) with per-file keys wrapped via RFC 9180 HPKE over X25519 — storage nodes never see your data. A hybrid X25519 + ML-KEM-768 primitive ships in `fula-crypto::hybrid_kem` for applications that want post-quantum wrapping today; the default client path is X25519-only while that migration is in flight.\n- **✅ Verified Streaming**: BLAKE3/Bao root hash plus per-chunk AAD binding ensures large files reassemble from exactly the bytes that were uploaded\n- **🧭 Private index (forest)**: an encrypted per-bucket index that maps real paths to scrambled storage keys — now a sharded HAMT (v7) borrowed from [rs-wnfs](https://github.com/wnfs-wg/rs-wnfs), so a bucket with millions of entries doesn't require downloading the whole index\n- **🔄 Conflict-Free Sync**: CRDT-based metadata for distributed updates\n- **📈 Efficient Indexing**: Prolly Trees for O(log n) bucket operations on the server side\n\n## How it works (plain English)\n\n**Encryption \u0026 storage.** Every file gets its own random 32-byte key. Files up to 768 KB are sealed as a single blob; anything larger is sliced into 256 KB pieces and each piece is sealed separately with a tag that names the file and the piece — so pieces can't be shuffled, swapped, or replayed across files. The real filename and folder are replaced by a random-looking ID before anything leaves your machine, and the per-file key is itself wrapped with your own keypair so only you can open it. A small encrypted index (the \"private forest\") remembers which scrambled ID belongs to which real filename; it lives in the same bucket but is itself encrypted, and for large libraries it's stored as a sharded hash-array-mapped-trie (HAMT) so the client only loads the pieces it needs.\n\n**Decryption.** Your personal key unwraps the per-file key, the client decrypts the blob (or reassembles and checks each piece against its tag and the file's BLAKE3 root hash), and you get your bytes back. The forest also pins a hash of the original plaintext, so tampering after upload is detected even if the server somehow produced a blob with a valid inner tag.\n\n**Sharing.** To share, the client re-wraps the file's key for the recipient's public key and attaches a short note — what they can do, when the share expires, and which path it covers. The result is a small token placed in the URL fragment after `#`, so the server never sees the key. The recipient pastes the link, their own key unwraps the token, and they fetch the encrypted bytes through a lightweight proxy endpoint (no S3 account required for the recipient). Your personal key stays private; you're only handing over that one file's lock. Shares come in two flavors: **temporal** (always resolves to the current version of the shared path) or **snapshot** (locked to the exact content hash at share time, refuses to serve a newer version).\n\n## 📖 Documentation\n\n- **[Introduction](https://functionland.github.io/fula-api/)** - Architecture, concepts, and how it works\n- **[API Reference](https://functionland.github.io/fula-api/api.html)** - Complete endpoint documentation with examples\n- **[SDK Examples](https://functionland.github.io/fula-api/sdk.html)** - Code examples for Rust, Python, JavaScript\n- **[Platform Guides](https://functionland.github.io/fula-api/platforms.html)** - Next.js, React Native, .NET, Flutter, Swift, Kotlin\n\n## Architecture\n\n```\n┌────────────────────────────────────────────────────────────────┐\n│                      Application Layer                         │\n│          (boto3, AWS SDK, Flutter/React-Native app, browser)   │\n├────────────────────────────────────────────────────────────────┤\n│           fula-client  /  fula-flutter  /  fula-js (WASM)      │\n│  ┌──────────────┬────────────────────┬──────────────────────┐  │\n│  │  Per-file    │  Chunked streaming │  Sharded HAMT v7     │  │\n│  │  AEAD +      │  (256 KB chunks,   │  private forest      │  │\n│  │  path        │   per-chunk AAD,   │  (encrypted index,   │  │\n│  │  obfuscation │   Bao root hash)   │   lazy shard load)   │  │\n│  └──────────────┴────────────────────┴──────────────────────┘  │\n├────────────────────────────────────────────────────────────────┤\n│                       Fula Gateway                             │\n│    ┌─────────────┬──────────────┬──────────────────────┐       │\n│    │    Auth     │ Rate Limiter │   S3 API Handlers    │       │\n│    └─────────────┴──────────────┴──────────────────────┘       │\n├────────────────────────────────────────────────────────────────┤\n│                        fula-core                               │\n│    ┌─────────────┬──────────────┬──────────────────────┐       │\n│    │Prolly Trees │    Buckets   │       CRDTs          │       │\n│    └─────────────┴──────────────┴──────────────────────┘       │\n├────────────────────────────────────────────────────────────────┤\n│                     fula-blockstore                            │\n│    ┌─────────────┬──────────────┬──────────────────────┐       │\n│    │    IPFS     │ IPFS Cluster │  FastCDC (IPFS-layer)│       │\n│    └─────────────┴──────────────┴──────────────────────┘       │\n├────────────────────────────────────────────────────────────────┤\n│                        fula-crypto                             │\n│  ┌───────────────┬──────────────┬──────────────┬────────────┐  │\n│  │  RFC 9180     │   BLAKE3     │     Bao      │ hybrid_kem │  │\n│  │  HPKE         │   hashing    │   verified   │ X25519 +   │  │\n│  │  (X25519 KEM) │              │   streaming  │ ML-KEM-768 │  │\n│  │               │              │              │ (opt-in)   │  │\n│  └───────────────┴──────────────┴──────────────┴────────────┘  │\n└────────────────────────────────────────────────────────────────┘\n```\n\nThe client stack (top) is where all encryption, obfuscation, and forest-index\nwork happens. The gateway and block-store layers never see plaintext, nor\nreal paths — they see random-looking content-addressed keys.\n\n## Quick Start\n\n### Using Docker Compose\n\n```bash\n# Clone the repository\ngit clone https://github.com/functionland/fula-api\ncd fula-api\n\n# Start the stack\ndocker-compose up -d\n\n# The gateway is now available at http://localhost:9000\n```\n\n### Using AWS CLI\n\nFula supports **AWS Signature V4** authentication, enabling full compatibility with standard S3 tools. Embed your JWT token in the access key with a `JWT:` prefix:\n\n```bash\n# Configure credentials (~/.aws/credentials)\ncat \u003e\u003e ~/.aws/credentials \u003c\u003c EOF\n[fula]\naws_access_key_id = JWT:your-jwt-token-here\naws_secret_access_key = not-used\nEOF\n\n# Use AWS CLI with Fula gateway\naws s3 mb s3://my-bucket --endpoint-url http://localhost:9000 --profile fula\naws s3 cp file.txt s3://my-bucket/ --endpoint-url http://localhost:9000 --profile fula\naws s3 ls s3://my-bucket/ --endpoint-url http://localhost:9000 --profile fula\n```\n\n### Using Python (boto3)\n\n```python\nimport boto3\n\n# Configure with JWT embedded in access key\ns3 = boto3.client('s3',\n    endpoint_url='http://localhost:9000',\n    aws_access_key_id=f'JWT:{jwt_token}',\n    aws_secret_access_key='not-used',\n    region_name='us-east-1'\n)\n\n# Use S3 API normally\ns3.create_bucket(Bucket='my-bucket')\ns3.put_object(Bucket='my-bucket', Key='hello.txt', Body=b'Hello World!')\n```\n\n### Using JavaScript (AWS SDK)\n\n```javascript\nimport { S3Client, PutObjectCommand } from \"@aws-sdk/client-s3\";\n\nconst s3 = new S3Client({\n  endpoint: \"http://localhost:9000\",\n  region: \"us-east-1\",\n  forcePathStyle: true,\n  credentials: {\n    accessKeyId: `JWT:${jwtToken}`,\n    secretAccessKey: \"not-used\"\n  }\n});\n\nawait s3.send(new PutObjectCommand({\n  Bucket: \"my-bucket\",\n  Key: \"hello.txt\",\n  Body: \"Hello World!\"\n}));\n```\n\n### Using the Rust Client SDK\n\n```rust\nuse fula_client::{FulaClient, Config};\n\n#[tokio::main]\nasync fn main() -\u003e anyhow::Result\u003c()\u003e {\n    let client = FulaClient::new(Config::new(\"http://localhost:9000\"))?;\n\n    // Create bucket\n    client.create_bucket(\"my-bucket\").await?;\n\n    // Upload object\n    client.put_object(\"my-bucket\", \"hello.txt\", b\"Hello, World!\").await?;\n\n    // Download object\n    let data = client.get_object(\"my-bucket\", \"hello.txt\").await?;\n    println!(\"{}\", String::from_utf8_lossy(\u0026data));\n\n    Ok(())\n}\n```\n\n## Features\n\n### S3 API Compatibility\n\n| Operation | Status |\n|-----------|--------|\n| CreateBucket | ✅ |\n| DeleteBucket | ✅ |\n| ListBuckets | ✅ |\n| HeadBucket | ✅ |\n| PutObject | ✅ |\n| GetObject | ✅ |\n| DeleteObject | ✅ |\n| HeadObject | ✅ |\n| CopyObject | ✅ |\n| ListObjectsV2 | ✅ |\n| CreateMultipartUpload | ✅ |\n| UploadPart | ✅ |\n| CompleteMultipartUpload | ✅ |\n| AbortMultipartUpload | ✅ |\n| ListParts | ✅ |\n| ListMultipartUploads | ✅ |\n\n### Client-Side Encryption\n\n```rust\nuse fula_client::{Config, EncryptedClient, EncryptionConfig};\n\n// FlatNamespace mode is default - complete structure hiding!\n// Server sees only random CID-like hashes (QmX7a8f3e2d1...)\nlet encryption = EncryptionConfig::new();\nlet client = EncryptedClient::new(\n    Config::new(\"http://localhost:9000\"),\n    encryption,\n)?;\n\n// Data encrypted with FlatNamespace - server cannot see folder structure\nclient.put_object_flat(\"bucket\", \"/photos/vacation/beach.jpg\", data, None).await?;\n\n// List files from encrypted PrivateForest index\nlet files = client.list_files_from_forest(\"bucket\").await?;\n```\n\n### Large File Uploads\n\n```rust\nuse fula_client::multipart::upload_large_file;\n\nlet etag = upload_large_file(\n    client,\n    \"bucket\",\n    \"large-file.bin\",\n    large_data,\n    Some(Box::new(|progress| {\n        println!(\"Progress: {:.1}%\", progress.percentage());\n    })),\n).await?;\n```\n\n## Crates\n\n| Crate | Description |\n|-------|-------------|\n| `fula-crypto` | Cryptographic primitives: RFC 9180 HPKE over X25519 (`hpke.rs`), AES-256-GCM/ChaCha20-Poly1305 AEAD (`symmetric.rs`), chunked streaming with per-chunk AAD (`chunked.rs`), BLAKE3 + Bao verified streaming (`streaming.rs`, `hashing.rs`), sharded HAMT v7 private forest (`sharded_hamt_forest.rs`, vendored `wnfs_hamt/`), share tokens (`sharing.rs`), key rotation (`rotation.rs`), opt-in hybrid PQ KEM X25519 + ML-KEM-768 (`hybrid_kem.rs`) |\n| `fula-blockstore` | IPFS block storage (content-addressed) |\n| `fula-core` | Storage engine: Prolly Trees for server-side bucket metadata, CRDT sync |\n| `fula-cli` | S3-compatible gateway server |\n| `fula-client` | Client SDK: encrypts, obfuscates paths, maintains the sharded HAMT forest, handles resumable chunked uploads, downgrade-gated reads |\n| `fula-flutter` | `flutter_rust_bridge` bindings over `fula-client` for the FxFiles app |\n| `fula-js` | WASM/TypeScript bindings (`wasm-bindgen`) for browsers — powers the web share-viewer and anything embedding `@functionland/fula-client` |\n\n## Configuration\n\nSee [.env.example](.env.example) for all configuration options.\n\nKey settings:\n\n```bash\n# Gateway\nFULA_HOST=0.0.0.0\nFULA_PORT=9000\n\n# IPFS\nIPFS_API_URL=http://localhost:5001\nCLUSTER_API_URL=http://localhost:9094\n\n# Authentication\nJWT_SECRET=your-secret-key\n```\n\n## Development\n\n### Building from Source\n\n```bash\n# Install Rust 1.83+\ncurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n# Build all crates\ncargo build --release\n\n# Run tests\ncargo test\n\n# Run the gateway\ncargo run --package fula-cli -- --no-auth\n```\n\n### Running Examples\n\n```bash\n# Basic usage\ncargo run --example basic_usage\n\n# Encryption\ncargo run --example encrypted_storage\n\n# Multipart upload\ncargo run --example multipart_upload\n\n# S3 compatibility guide\ncargo run --example s3_compatible\n\n# Security verification\ncargo run --example security_verification\n\n# Sharing Demo\ncargo run --example sharing_demo\n\n# Metadata Privacy\ncargo run --example metadata_privacy\n\n# Metadata fetch only\ncargo run --example file_manager_demo\n\n# FlatNamespace (maximum privacy - complete structure hiding)\ncargo run --example flat_namespace_demo\n\n\n```\n\n## Security\n\n### 🔐 Cryptographic Primitives\n\n| Component | Algorithm | Where it's used |\n|-----------|-----------|-----------------|\n| Symmetric AEAD (content) | AES-256-GCM (default), ChaCha20-Poly1305 | Per-file and per-chunk content encryption, 12-byte random nonce, 16-byte tag, AAD `fula:v4:content:{storage_key}` (single) / `fula:v4:chunk:{storage_key}:{index}` (chunked) |\n| Key Encapsulation (KEM) | RFC 9180 HPKE over X25519 (HkdfSha256, ChaCha20-Poly1305) | Wrapping the per-file DEK for the owner's keypair and for share recipients; DEK-wrap AAD = `fula:v2:dek-wrap` |\n| Integrity | BLAKE3 + Bao verified streaming | Root hash of the plaintext of chunked files; checked at `finalize_and_verify` |\n| Forest integrity pin | Unkeyed BLAKE3 over plaintext | Stored in the forest entry so a swap of tagged-but-wrong ciphertext still fails (audit finding H-1) |\n| Version pin | `min_version: u8` in forest entry | Rejects downgrade to pre-AAD blobs after a v4 upload (audit finding H-2) |\n| Post-quantum KEM (opt-in) | `fula_crypto::hybrid_kem` — X25519 + ML-KEM-768 (libcrux-ml-kem, NIST FIPS 203) | Available as a primitive; **not** wired into the default `EncryptedClient` wrap path yet. Applications can use it directly if they want PQ wrapping today. |\n\n```rust\n// Opt-in hybrid PQ wrap — standalone primitive, not the default client path.\nuse fula_crypto::{HybridKeyPair, hybrid_encapsulate, hybrid_decapsulate};\n\nlet keypair = HybridKeyPair::generate();\nlet (encapsulated_key, shared_secret) = hybrid_encapsulate(keypair.public_key())?;\nlet recovered = hybrid_decapsulate(\u0026encapsulated_key, keypair.secret_key())?;\nassert_eq!(shared_secret, recovered);\n```\n\n### Trust Model\n\n- **Storage nodes are untrusted**: All sensitive data is encrypted client-side\n- **Gateway is trusted for routing**: But never sees encryption keys\n- **Keys never leave the client**: HPKE ensures end-to-end encryption\n- **Per-user bucket isolation**: Each user's buckets are automatically namespaced - multiple users can have buckets with the same name without conflicts\n\n### Key Management\n\n- Generate keys locally using `EncryptionConfig::new()` (uses FlatNamespace by default)\n- Complete structure hiding - server cannot see folder/file relationships\n- Export/backup secret keys securely\n- Lost keys = lost data (no recovery possible)\n\n### Privacy Notice\n\n⚠️ **Important**: For private data, always use the **Encrypted Client SDK** (`EncryptedClient`).\n\nRaw S3 tools (AWS CLI, boto3) do NOT encrypt data - they upload plaintext that gateway operators can see.\n\n**What's encrypted** (with `EncryptedClient` in its default `FlatNamespace` mode):\n- ✅ File content (AEAD with per-file DEK; chunked files also carry per-chunk AAD)\n- ✅ File names and folder paths (server only sees CID-like `Qm…` keys; the forest index that maps the real path back is itself encrypted)\n- ✅ Directory structure (no `/` in storage keys; folder membership is only visible inside the encrypted forest)\n- ✅ User IDs (hashed via BLAKE3 KDF + path-specific key derivation)\n- ✅ Listings: the `list_files_from_forest` path decrypts the forest client-side; the server cannot answer `ls` queries with structural info\n\n**What remains visible to the gateway**:\n- ⚠️ Bucket names (not encrypted by design — they're routing identifiers)\n- ⚠️ Approximate ciphertext sizes (per-file for single-object; per-chunk for chunked — the 256 KB chunking partially smooths this for large files)\n- ⚠️ Request timestamps and access patterns\n- ⚠️ The existence of chunk objects at `{storage_key}.chunks/{index:08}` (the pattern reveals \"this file is chunked\" and roughly how many chunks)\n\nSee [docs/PRIVACY.md](docs/PRIVACY.md) for full privacy policy.\n\n### Large File Support (WNFS-inspired)\n\nChunking is **automatic**. Anything larger than 768 KB (the `CHUNKED_THRESHOLD`, also the IPFS block-size safety ceiling) is split into 256 KB chunks by default, each sealed with its own AEAD tag bound to the file and chunk index (`fula:v4:chunk:{storage_key}:{index}`). You keep calling `put_object_encrypted`; the client picks the right path:\n\n```rust\nuse fula_client::EncryptedClient;\n\n// Same API for small and large files — chunking is automatic.\nlet data = std::fs::read(\"movie.mp4\")?;\nclient.put_object_encrypted(\"my-bucket\", \"/videos/movie.mp4\", \u0026data).await?;\n\n// Partial read — only downloads the chunks covering the requested range.\nlet partial = client.get_object_range(\n    \"my-bucket\",\n    \"/videos/movie.mp4\",\n    1024 * 1024,  // offset: 1 MiB\n    1024 * 1024,  // length: 1 MiB\n).await?;\n\n// Explicit chunked API is still available if you want to override the\n// default chunk size (clamped to [64 KB, 768 KB]):\nclient.put_object_chunked(\"my-bucket\", \"/videos/movie.mp4\", \u0026data, Some(256 * 1024)).await?;\n```\n\n**Benefits:**\n- Memory efficient: chunks are decrypted and written one at a time (bounded window = 16 concurrent fetches)\n- Partial reads: download only the bytes you need\n- Resumable: failed uploads can restart from the last chunk (see `put_object_encrypted_resumable`)\n- Integrity: per-chunk AEAD + Bao root hash over the whole plaintext, verified on `finalize`\n- Downgrade-proof: the forest entry pins `min_version = 4`, so an attacker-authored legacy (no-AAD) blob at the same storage key is rejected on read (H-2)\n\nSee [docs/wnfs-borrowed-features.md](docs/wnfs-borrowed-features.md) for implementation details.\n\n## Production Deployment\n\nFor production Ubuntu deployments with security hardening:\n\n```bash\n# Download and run the installer\ncurl -fsSL https://raw.githubusercontent.com/functionland/fula-api/main/install.sh | sudo bash\n```\n\nThe installer will:\n- Install Docker and dependencies\n- Configure nginx with TLS (Let's Encrypt)\n- Set up rate limiting and fail2ban\n- Configure firewall (UFW)\n- Create systemd service\n- Optionally set up local IPFS node\n\nSee [install.sh](install.sh) for details.\n\n## License\n\nLicensed under either of:\n\n- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE))\n- MIT license ([LICENSE-MIT](LICENSE-MIT))\n\nat your option.\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.\n\n## Acknowledgments\n\n- [IPFS](https://ipfs.io/) - The InterPlanetary File System\n- [IPFS Cluster](https://cluster.ipfs.io/) - Pinset orchestration\n- [rs-wnfs](https://github.com/wnfs-wg/rs-wnfs) - HAMT implementation reference\n- [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) - Fast cryptographic hashing\n- [Bao](https://github.com/oconnor663/bao) - Verified streaming\n\n\n## Update\n\n```\n~/fula-api# git pull\n~/fula-api# rsync -a --delete /root/fula-api/ /opt/fula-api/\n~/fula-api# cp docker-compose.yml /etc/fula/\ncd /opt/fula-api\n/opt/fula-api# docker-compose -f /etc/fula/docker-compose.yml build --no-cache gateway\nsystemctl restart fula-gateway\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunctionland%2Ffula-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffunctionland%2Ffula-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffunctionland%2Ffula-api/lists"}