Skip to content

Commit 05228d0

Browse files
committed
fix: merge missing changes on GCS
2 parents 8139ac4 + 8043dda commit 05228d0

7 files changed

Lines changed: 79 additions & 35 deletions

File tree

README.md

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,30 +40,35 @@
4040

4141
[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/MGW3k1?referralCode=OEHlEK&utm_medium=integration&utm_source=template&utm_campaign=generic)
4242

43-
🚀 **An open-source Go implementation of the Expo Updates protocol, designed for production with support for cloud storage like S3 and CDN integration, delivering fast and reliable OTA updates for React Native apps.**
43+
Or with Docker:
4444

45-
## ⚠️ Disclaimer
45+
```bash
46+
docker run -p 3000:3000 \
47+
-e STORAGE_MODE=s3 \
48+
-e S3_BUCKET_NAME=my-bucket \
49+
-e AWS_REGION=us-east-1 \
50+
axelmarciano/expo-open-ota
51+
```
4652

47-
**Expo Open OTA is not officially supported or affiliated with [Expo](https://expo.dev/).**
48-
This is an independent open-source project.
53+
Then configure your Expo app to point to your server — see the [full documentation](https://axelmarciano.github.io/expo-open-ota/).
4954

50-
## 📖 Documentation
55+
## Storage Options
5156

52-
The full documentation is available at:
53-
➡️ [Documentation](https://axelmarciano.github.io/expo-open-ota/)
57+
| Provider | Mode | Asset Delivery |
58+
|----------|------|----------------|
59+
| **Amazon S3** | `STORAGE_MODE=s3` | Direct or CloudFront CDN |
60+
| **Google Cloud Storage** | `STORAGE_MODE=gcs` | GCS signed URLs |
61+
| **S3-compatible** (R2, MinIO, etc.) | `STORAGE_MODE=s3` + `AWS_BASE_ENDPOINT` | Direct |
62+
| **Local file system** | `STORAGE_MODE=local` | Direct (dev only) |
5463

55-
## 🛠 Features
64+
## Disclaimer
5665

57-
- **Self-hosted OTA update server** for Expo applications.
58-
- **Cloud storage support**: AWS S3, local storage, and more.
59-
- **CDN integration**: Optimized for CloudFront and other CDN providers.
60-
- **Secure key management**: Supports AWS Secrets Manager and environment-based key storage.
61-
- **Production-ready**: Designed for scalability and performance.
66+
Expo Open OTA is **not officially supported or affiliated with [Expo](https://expo.dev/)**. This is an independent open-source project.
6267

63-
## 📜 License
68+
## License
6469

65-
This project is licensed under the MIT License - see the [LICENSE](./LICENSE.md) file for details.
70+
MIT see [LICENSE](./LICENSE.md).
6671

6772
## Contact
6873

69-
✉️ [E-mail](mailto:expoopenota@gmail.com)
74+
[expoopenota@gmail.com](mailto:expoopenota@gmail.com)

apps/docs/docs/getting-started/introduction.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ sidebar_position: 1
44

55
# Introduction
66

7-
**Expo Open OTA** is an open-source project that allows you to host your own updates server for your Expo application. It is a self-hosted alternative to the official Expo OTA service, implementing the [Expo Updates protocol](https://docs.expo.dev/technical-specs/expo-updates-1/).
7+
**Expo Open OTA** is an open-source, multi-cloud OTA update server for Expo and React Native applications. It implements the [Expo Updates protocol](https://docs.expo.dev/technical-specs/expo-updates-1/) and supports **AWS S3**, **Google Cloud Storage**, and any **S3-compatible** provider (Cloudflare R2, MinIO, DigitalOcean Spaces).
88

99
:::warning
1010
**Expo Open OTA** is not affiliated with Expo. It is an independent open-source project.
@@ -30,10 +30,10 @@ When an update is available, a list of assets is sent back to the client. These
3030
- Signs and compresses the files.
3131
- Returns the required assets to the Expo client.
3232

33-
If a CDN is configured, the returned URL is a pre-signed link pointing to a cdn endpoint. Otherwise, the server returns the asset directly.
33+
If a CDN is configured (CloudFront) or if using GCS, the returned URL is a pre-signed/signed link and the client downloads directly from the storage provider. Otherwise, the server returns the asset directly.
3434

3535
### 3. `/requestUploadUrl` & `/uploadLocalFile`
36-
These routes are used by the `eoas` package to publish updates to the chosen storage solution, whether it's S3 or a local file system.
36+
These routes are used by the `eoas` package to publish updates to the chosen storage solution, whether it's S3, Google Cloud Storage, or a local file system.
3737
`/uploadLocalFile` is used to upload the file to the server when [storage mode](/docs/server-configuration/storage?storage=local) is set to `local`.
3838

3939
## Why Self-Host Your OTA Update server?

apps/docs/docs/server-configuration/storage.mdx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,41 @@ import TabItem from '@theme/TabItem';
7474
```bash title=".env"
7575
STORAGE_MODE=gcs
7676
GCS_BUCKET_NAME=your-bucket-name
77+
GOOGLE_APPLICATION_CREDENTIALS_B64=<base64-encoded service account JSON>
7778
```
7879

79-
To generate signed URLs for uploads and asset redirects, provide your service account credentials as a base64-encoded JSON:
80-
```bash title=".env"
81-
GOOGLE_APPLICATION_CREDENTIALS_B64=<base64-encoded service account JSON>
80+
#### Setting up GCP credentials
81+
82+
1. In the GCP Console, go to **IAM & Admin > Service Accounts**
83+
2. Create a new service account (or use an existing one)
84+
3. Go to your bucket in **Cloud Storage > Buckets**, open the **Permissions** tab
85+
4. Click **Grant Access**, add your service account with the **Storage Admin** role
86+
5. Back in Service Accounts, go to **Keys > Add Key > Create new key > JSON**
87+
6. Encode the downloaded JSON file to base64:
88+
89+
```bash
90+
base64 -i /path/to/service-account.json | tr -d '\n'
8291
```
8392

84-
The service account must have at minimum `storage.objectCreator` and `storage.objectAdmin` permissions on your bucket. You don't need to make objects public — the server generates short-lived signed URLs for all file access.
93+
7. Set the output as `GOOGLE_APPLICATION_CREDENTIALS_B64` in your `.env`
94+
95+
:::tip
96+
The base64 credential is used both for authenticating API calls (read, write, delete objects) and for generating signed URLs.
97+
:::
98+
99+
#### How asset delivery works
100+
101+
Unlike S3 where you can optionally configure CloudFront as a CDN, GCS uses **direct signed URLs** for asset delivery. When a client requests an update asset, the server generates a short-lived signed URL (15 minutes) and redirects the client to download the file directly from GCS — the server never proxies the file content itself.
102+
103+
This is automatic when `GOOGLE_APPLICATION_CREDENTIALS_B64` is set. No additional CDN configuration is needed.
104+
105+
#### Permissions
106+
107+
The service account needs at minimum the **Storage Admin** role on your bucket. This covers:
108+
- `storage.objects.get` — reading objects
109+
- `storage.objects.list` — listing branches, runtime versions, and updates
110+
- `storage.objects.create` — uploading new updates
111+
- `storage.objects.delete` — removing updates
112+
- Signed URL generation for asset delivery
85113
</TabItem>
86114
</Tabs>

apps/docs/docusaurus.config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type * as Preset from '@docusaurus/preset-classic';
77
const config: Config = {
88
title: 'Expo Open OTA',
99
tagline:
10-
'An open-source Go implementation of the Expo Updates protocol, designed for production with support for cloud storage like S3 and CDN integration, delivering fast and reliable OTA updates for React Native apps.',
10+
'Self-hosted Expo updates for React Native. Multi-cloud (AWS S3, Google Cloud Storage), CDN-ready, and production-grade — an open-source Go server implementing the Expo Updates protocol.',
1111
favicon: 'img/favicon.ico',
1212
scripts: [
1313
{
@@ -48,7 +48,7 @@ const config: Config = {
4848
// Please change this to your repo.
4949
// Remove this to remove the "edit this page" links.
5050
editUrl:
51-
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
51+
'https://github.com/axelmarciano/expo-open-ota/tree/main/apps/docs/',
5252
},
5353
blog: {
5454
showReadingTime: true,
@@ -59,7 +59,7 @@ const config: Config = {
5959
// Please change this to your repo.
6060
// Remove this to remove the "edit this page" links.
6161
editUrl:
62-
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
62+
'https://github.com/axelmarciano/expo-open-ota/tree/main/apps/docs/',
6363
// Useful options to enforce blogging best practices
6464
onInlineTags: 'warn',
6565
onInlineAuthors: 'warn',

apps/docs/src/components/HomepageFeatures/index.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,29 @@ type FeatureItem = {
1010

1111
const FeatureList: FeatureItem[] = [
1212
{
13-
title: '⚙️ Ready for production in 10 minutes',
13+
title: '⚙️ Production-ready in 10 minutes',
1414
description: (
1515
<>
16-
No database, no complex setup—Expo Open OTA is designed for seamless production use. It fully leverages Expo’s configuration, handling <strong>release channels</strong>, <strong>branches</strong>, and <strong>runtime version fingerprints</strong> out of the box. Just connect your cloud storage (S3) and you're ready to go!
16+
No database, no complex setup. Connect your cloud storage — <strong>AWS S3</strong>, <strong>Google Cloud Storage</strong>, or any S3-compatible provider — and you’re live. Handles release channels, branches, and runtime versions out of the box.
1717
</>
1818
),
1919
},
2020
{
21-
title: '🚀 EOAS: One Command to Publish & Configure',
21+
title: '🚀 One Command to Publish',
2222
description: (
2323
<>
24-
Say goodbye to manual setup! Our <code>eoas</code> NPM package automates everythingrun <code>npx eoas init</code> to configure your project, and <code>npx eoas publish</code> to push updates effortlessly from your CI/CD pipeline. No extra scripts, no hassle.
24+
The <code>eoas</code> CLI automates everythingrun <code>npx eoas init</code> to configure your project, and <code>npx eoas publish</code> to push updates from your CI/CD pipeline. No extra scripts, no hassle.
2525
</>
2626
),
2727
},
2828
{
29-
title: '⚡ CDN Delivery',
29+
title: '⚡ Fast Asset Delivery',
3030
description: (
3131
<>
32-
Your assets, delivered at lightning speed. Expo Open OTA serves static assets through a CDN for maximum performance. Currently supporting AWS CloudFront, with upcoming support for Cloudflare and more—so your users get updates instantly, wherever they are.
32+
Assets served at the edge. Deliver updates via <strong>CloudFront CDN</strong> or <strong>GCS signed URLs</strong> your users get updates instantly, wherever they are. No public bucket access needed.
3333
</>
3434
),
3535
},
36-
3736
];
3837

3938

internal/auth/auth.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import (
55
"expo-open-ota/config"
66
"expo-open-ota/internal/services"
77
"fmt"
8-
"github.com/golang-jwt/jwt/v5"
98
"time"
9+
10+
"github.com/golang-jwt/jwt/v5"
1011
)
1112

1213
type Auth struct {

internal/services/gcp.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"cloud.google.com/go/storage"
14+
"google.golang.org/api/option"
1415
)
1516

1617
var (
@@ -22,7 +23,17 @@ var (
2223
func GetGCSClient() (*storage.Client, error) {
2324
initGCSClient.Do(func() {
2425
ctx := context.Background()
25-
gcsClient, gcsClientErr = storage.NewClient(ctx)
26+
var opts []option.ClientOption
27+
b64Creds := config.GetEnv("GOOGLE_APPLICATION_CREDENTIALS_B64")
28+
if b64Creds != "" {
29+
creds, err := base64.StdEncoding.DecodeString(b64Creds)
30+
if err != nil {
31+
gcsClientErr = fmt.Errorf("error decoding GOOGLE_APPLICATION_CREDENTIALS_B64: %w", err)
32+
return
33+
}
34+
opts = append(opts, option.WithCredentialsJSON(creds))
35+
}
36+
gcsClient, gcsClientErr = storage.NewClient(ctx, opts...)
2637
if gcsClientErr != nil {
2738
gcsClientErr = fmt.Errorf("error initializing GCS client: %w", gcsClientErr)
2839
}

0 commit comments

Comments
 (0)