# Migrating from Laravel Vapor

Bref and [Laravel Vapor](https://vapor.laravel.com/) both run Laravel on AWS Lambda, in your own AWS account, with the same building blocks: Lambda, API Gateway, CloudFront, SQS, S3, RDS. Migrating from Vapor to Bref is more a configuration change than a rewrite.

This guide uses [Bref Cloud](https://bref.sh/cloud) as the main path, because it replaces what the Vapor dashboard does: deployments, environment variables and secrets, databases, logs, metrics, queues and commands. Bref Cloud is optional though, every section also mentions how to proceed without it.

## What changes

What you gain:

- **A proven, actively maintained project**: Bref was created in 2017 and is used by thousands of companies. It handles tens of billions of requests every month across all users, and it is referenced in the AWS Lambda documentation.
- **No lock-in**: Bref is open source and deploys standard CloudFormation stacks to your AWS account. If you stop using Bref, everything keeps running. Bref Cloud is optional and has a free plan for personal projects ([pricing](https://bref.sh/cloud#pricing)).
- **Full access to AWS**: you are not limited in the AWS services and features you can use. Functions can react to any Lambda event (S3, EventBridge, SNS…), and `serverless.yml` accepts [any CloudFormation resource](https://bref.sh/docs/environment/serverless-yml#resources) next to the application.
- **Simpler assets**: assets are served on the same domain as the application, `ASSET_URL` is no longer needed.

What you lose:

- **Deploy hooks**: Vapor runs the `deploy` hooks (for example migrations) before the new version receives traffic. With Bref, you run the migrations after `bref deploy`. That being said, in both cases old and new code use the same database for a short time: the `web`, `artisan` and queue functions are updated one after the other, and requests and jobs in progress finish on the old version. This is also true with Vapor: migrations should be backward-compatible.
- **Rollbacks**: there is no `vapor rollback` command. To roll back, redeploy the previous commit.
- **Infrastructure commands**: Vapor creates caches and firewalls. Bref Cloud creates databases and networks, but not caches and firewalls yet. You need to create these resources in the AWS console or in `serverless.yml`. This guide reuses the resources created by Vapor, so this only matters for new resources.
- **A shorter configuration file**: `serverless.yml` is longer than `vapor.yml`, because it describes each function explicitly.

> [!TIP]
>
> Need help with the migration? [Bref support](https://bref.sh/support) includes direct help from Matthieu, creator of Bref and [AWS Serverless Hero](https://builder.aws.com/community/heroes/MatthieuNapoli).

## Migration strategy

The recommended strategy is to **keep the long-lived infrastructure and migrate the application**:

- **Keep**: the database (RDS or Aurora), the Redis cache (ElastiCache), the S3 storage bucket, the network (VPC and NAT gateway), the domain and its HTTPS certificate. Vapor created these resources in your AWS account and they will continue to work without Vapor.
- **Recreate with Bref**: the Lambda functions, the HTTP layer (API Gateway and CloudFront), the SQS queues and the scheduler. These are cheap, stateless and quick to recreate.

The migration then happens without downtime (except in one case, see [Domain and DNS switch](#domain-and-dns-switch)):

1. Deploy the application with Bref next to the Vapor environment, connected to the same database, cache and bucket, without the scheduler.
2. Test it on the URL generated by Bref.
3. Move the scheduler from Vapor to Bref (see [Scheduler](#scheduler)), and switch the DNS record of your domain to the Bref deployment.
4. Delete the Vapor environment (see [Clean up](#clean-up)).

Recreating the database and the cache is also possible (dump and restore), but it requires a maintenance window to avoid losing data. Reusing them is simpler.

> [!TIP]
>
> Lower the TTL of your DNS records a day before switching, so that the switch propagates quickly.

## Before you start

- [Create a Bref Cloud account](https://bref.cloud/register) and connect **the AWS account where Vapor deploys** your application. Bref Cloud connects [via an IAM role](https://bref.sh/docs/cloud-security), the same way Vapor does.
- [Install the `bref` CLI](https://bref.sh/docs/cloud-getting-started) and run `bref login`.
- Download the environment variables of the Vapor environment, you will need them later:

  ```bash
  vapor env:pull production
  ```

  This creates a `.env.production` file. Do not commit it.
- Open the [AWS Lambda console](https://console.aws.amazon.com/lambda/) and find the `vapor-<project>-<environment>` function. Its configuration is a useful reference during the migration: memory, timeout, VPC subnets and security groups, and the environment variables injected by Vapor (database host, Redis host, bucket name, etc.).

If you do not want to use Bref Cloud, [follow the Setup guide](https://bref.sh/docs/setup) to create AWS credentials, install the `serverless` CLI, and replace `bref deploy` with `serverless deploy` throughout this guide.

## Install Bref

Replace the Vapor packages with the Bref packages:

```bash
composer remove laravel/vapor-core laravel/vapor-cli
composer require bref/bref bref/laravel-bridge --update-with-dependencies
npm install --save-dev serverless-lift
```

The [Lift plugin](https://github.com/getlift/lift) provides the `constructs` section of `serverless.yml`, used below for queues and the website.

If your project uses them, also remove `laravel/vapor-ui` (Bref Cloud replaces it, see [Monitoring](#monitoring-logs-and-commands)) and the `laravel-vapor` npm package (see [Assets](#assets) and [File uploads](#file-uploads)).

Then create the `serverless.yml` file, which replaces `vapor.yml`:

```bash
php artisan vendor:publish --tag=serverless-config
```

The `bref/laravel-bridge` package does what `laravel/vapor-core` did at runtime: it moves the storage directory to `/tmp`, sends logs to `stderr` (CloudWatch), trusts the API Gateway proxy headers, and uses the cookie session driver when the file driver is configured ([details](https://bref.sh/docs/laravel/getting-started#how-it-works)). No code change is needed for that.

Vapor-specific files can be deleted once the migration is complete: `vapor.yml`, `config/vapor.php` and `.vaporignore`. Keep the `uploadFiles` method of your `UserPolicy` if you use [file uploads](#file-uploads), Bref uses the same gate.

## From vapor.yml to serverless.yml

Here is a typical `vapor.yml`:

```yml filename="vapor.yml"
id: 12345
name: my-app
environments:
    production:
        runtime: 'php-8.4:al2023'
        memory: 1024
        cli-memory: 512
        queue-memory: 1024
        timeout: 28
        domain: example.com
        database: my-app-db
        cache: my-app-cache
        storage: my-app-storage
        queues:
            - default
        scheduler: true
        build:
            - 'composer install --no-dev'
            - 'npm ci && npm run build'
        deploy:
            - 'php artisan migrate --force'
```

And the equivalent `serverless.yml`, based on the file published by `bref/laravel-bridge`:

```yml filename="serverless.yml"
service: my-app

bref:
    team: my-team # your Bref Cloud team

provider:
    name: aws
    region: us-east-1 # the region of your Vapor project
    environment:
        APP_ENV: ${param:appEnv}
        APP_KEY: ${ssm:/my-app/${sls:stage}/APP_KEY}
        # ... see the "Environment variables and secrets" section
    # Run in the Vapor network to reach the private database and cache
    # (see the "Database" section)
    vpc:
        securityGroupIds:
            - sg-0123456789abcdef0
        subnetIds:
            - subnet-0123456789abcdef0
            - subnet-abcdef0123456789

functions:
    web:
        handler: public/index.php
        runtime: php-84-fpm
        memorySize: 1024 # vapor.yml `memory`
        timeout: 28 # vapor.yml `timeout`
        events:
            - httpApi: '*'

    artisan:
        handler: artisan
        runtime: php-84-console
        memorySize: 1024 # vapor.yml `cli-memory`
        timeout: 720 # vapor.yml `cli-timeout`
        events:
            # vapor.yml `scheduler: true`
            # Add it when you switch from Vapor (see the "Scheduler" section)
            - schedule:
                  rate: rate(1 minute)
                  input: '"schedule:run"'

constructs:
    # vapor.yml `queues`
    jobs:
        type: queue
        worker:
            handler: Bref\LaravelBridge\Queue\QueueHandler
            runtime: php-84
            memorySize: 1024 # vapor.yml `queue-memory`
            timeout: 60 # vapor.yml `queue-timeout`

    # vapor.yml `domain` and assets (see the "Assets" and "Domain" sections)
    website:
        type: server-side-website
        versionedAssets: true
        assets:
            '/build/*': public/build
            '/favicon.ico': public/favicon.ico
            '/robots.txt': public/robots.txt
        domain: example.com
        certificate: arn:aws:acm:us-east-1:123456789012:certificate/...

params:
    default:
        appEnv: ${sls:stage}
    prod:
        appEnv: production

package:
    patterns: # replaces `.vaporignore`
        - '!.env*'
        - '!node_modules/**'
        - '!public/build/**'
        - 'public/build/manifest.json'
        - '!resources/js/**'
        - '!resources/css/**'
        - '!storage/**'
        - '!tests/**'

plugins:
    - ./vendor/bref/bref
    - serverless-lift
```

Vapor deploys one Lambda function for HTTP, one for the CLI and one for queues. Bref does the same, but each function is explicit in `serverless.yml`: `web`, `artisan` and the queue worker.

Vapor environments (`production`, `staging`) become Bref environments: `bref deploy --env=production`. Values that differ between environments go in the `params` section ([Laravel environments](https://bref.sh/docs/laravel/environments)).

### vapor.yml reference

| `vapor.yml` | `serverless.yml` |
|---|---|
| `runtime: php-8.4:al2023` | `runtime: php-84-fpm` (web), `php-84-console` (artisan), `php-84` (queue worker). [Runtimes](https://bref.sh/docs/runtimes) |
| `runtime: php-8.4:al2023-arm` | `architecture: arm64` on each function. [ARM runtimes](https://bref.sh/docs/runtimes#arm-runtimes) |
| `runtime: docker` | Deploy the `bref/php-84` Docker image. [Deploying Docker images](https://bref.sh/docs/deploy/docker) |
| `memory`, `cli-memory`, `queue-memory` | `memorySize` on the `web`, `artisan` and queue worker functions (1024MB is the recommended default). [Function configuration](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/functions.md#configuration) |
| `timeout`, `cli-timeout`, `queue-timeout` | `timeout` on each function (max 28 seconds for `web`, because of API Gateway). [Function configuration](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/functions.md#configuration) |
| `concurrency` | `reservedConcurrency` on the `web` function. [Function configuration](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/functions.md#configuration) |
| `queue-concurrency` | `maxConcurrency` on the queue construct. [Lift queue documentation](https://github.com/getlift/lift/blob/master/docs/queue.md#max-concurrency) |
| `queues` | One `queue` construct per queue. [Laravel Queues](https://bref.sh/docs/laravel/queues) |
| `scheduler: true` | `schedule` event on the `artisan` function. [Cron tasks](https://bref.sh/docs/use-cases/cron) |
| `warm` | `warmer` schedule event, or provisioned concurrency. [Cold starts](https://bref.sh/docs/environment/cold-starts) |
| `database`, `cache`, `network`, `subnets`, `security-groups` | Environment variables and `provider.vpc`. [Database](#database) |
| `storage` | `AWS_BUCKET` and IAM permissions. [File storage](#file-storage) |
| `domain`, `asset-domain` | `domain` and `certificate` on the `server-side-website` construct. [Domain](#domain-and-dns-switch) |
| `octane: true` | `handler: Bref\LaravelBridge\Http\OctaneHandler` with `runtime: php-84`. [Laravel Octane](https://bref.sh/docs/laravel/octane) |
| `octane-database-session-persist` | `OCTANE_PERSIST_DATABASE_SESSIONS` environment variable. [Persistent database connections](https://bref.sh/docs/laravel/octane#persistent-database-connections) |
| `gateway-version: 2` | `httpApi` event (the default). `gateway-version: 1` is the `http` event. [HTTP applications](https://bref.sh/docs/use-cases/http/advanced-use-cases) |
| `balancer` | `alb` event. Bref does not create the load balancer, create it in the AWS console or in the `resources` section. [Application Load Balancer](https://bref.sh/docs/use-cases/http/advanced-use-cases#application-load-balancer) |
| `firewall` | AWS WAF. Bref does not create it, attach a WAF web ACL to the CloudFront distribution or the API Gateway in [the AWS WAF console](https://console.aws.amazon.com/wafv2/) |
| `tmp-storage` | `ephemeralStorageSize` on the function. [Ephemeral storage](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/functions.md#ephemeral-storage) |
| `build` | Steps of your CI pipeline before `bref deploy`. [Deploying from GitHub Actions](https://bref.sh/docs/cloud-deploy#deploying-from-github-actions) |
| `deploy` | `bref command "migrate --force"` after `bref deploy`. [Database migrations](#database) |
| `ignore` and `.vaporignore` | `package.patterns`. [Exclusions](https://bref.sh/docs/environment/serverless-yml#exclusions) |
| `separate-vendor` | Not needed. Use [Docker images](https://bref.sh/docs/deploy/docker) if the application exceeds the 250MB limit |
| `dockerfile`, `docker-build-args` | `provider.ecr.images`. [Deploying Docker images](https://bref.sh/docs/deploy/docker) |

Bref supports Laravel Octane, Docker and ARM, like Vapor. Note that [extra PHP extensions](https://bref.sh/docs/environment/php#extra-extensions) are not available for ARM.

## Environment variables and secrets

Vapor stores environment variables and secrets in its dashboard and injects them at deployment. With Bref, they live in `serverless.yml` and in your AWS account:

- **Plain values** go in `provider.environment` in `serverless.yml`, with [parameters](https://bref.sh/docs/laravel/environments#configure-each-environment) for values that change between environments.
- **Secrets** (`APP_KEY`, database password, API tokens) are created in Bref Cloud, in the "Secrets" tab of the environment or with `bref secret:create`. Bref Cloud stores them as SSM parameters in your AWS account, under `/<app>/<environment>/<name>`, and you reference them in `serverless.yml`. They are resolved at deployment time:

  ```yml filename="serverless.yml"
  provider:
      environment:
          APP_KEY: ${ssm:/my-app/${sls:stage}/APP_KEY}
          DB_PASSWORD: ${ssm:/my-app/${sls:stage}/DB_PASSWORD}
  ```

Go through the `.env.production` file pulled from Vapor and sort the variables into these two groups. **Keep the same `APP_KEY`**: it encrypts sessions, cookies and any encrypted data in your database.

Drop the variables that Vapor injected or that only make sense for Vapor: `VAPOR_*`, `ASSET_URL`, `MIX_URL`, `SQS_PREFIX`, `DYNAMODB_CACHE_TABLE` (unless sessions are stored in DynamoDB, see [Cache and sessions](#cache-and-sessions)). Replace `SQS_QUEUE` with the URL of the new queue (see [Queues](#queues)).

**Without Bref Cloud**: create the SSM parameters with the AWS CLI or the AWS console, the `${ssm:...}` syntax works the same. [Read more about secrets](https://bref.sh/docs/environment/variables#secrets).

A few details:

- Lambda limits environment variables to 4KB in total. If large secrets (long tokens, private keys) exceed that limit, load them at runtime instead of at deployment time with the [`bref/secrets-loader` package](https://bref.sh/docs/environment/variables#at-runtime) and the `bref-ssm:` prefix.
- If you use an **encrypted environment file** (`.env.production.encrypted` with `LARAVEL_ENV_ENCRYPTION_KEY`), the simplest is to move its values to `serverless.yml` and Bref Cloud secrets. Alternatively, decrypt it in your CI pipeline with `php artisan env:decrypt` and deploy the resulting `.env` file with the application.
- Vapor sets `MYSQL_ATTR_SSL_CA=/var/task/rds-combined-ca-bundle.pem` for RDS MySQL databases, using a certificate bundle shipped by Vapor. If your database requires SSL connections, [download the RDS certificate bundle](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html) into your project (for example `certs/global-bundle.pem`) and set `MYSQL_ATTR_SSL_CA=/var/task/certs/global-bundle.pem`.

## Database

Keep the database created by Vapor. It is a regular RDS or Aurora database in your AWS account.

In Bref Cloud, open your AWS account page and click "Scan for apps and databases": existing databases are imported and listed in the [Databases page](https://bref.cloud/databases) with their hostname and port, next to the databases created by Bref Cloud.

Then configure Laravel with the same variables Vapor injected:

```yml filename="serverless.yml"
provider:
    environment:
        DB_CONNECTION: mysql
        DB_HOST: my-app-db.abc123.us-east-1.rds.amazonaws.com
        DB_PORT: 3306
        DB_DATABASE: vapor
        DB_USERNAME: vapor
        DB_PASSWORD: ${ssm:/my-app/${sls:stage}/DB_PASSWORD}
```

The values are in the `.env.production` file, in the Vapor dashboard (in the database details), or in the environment variables of the Vapor Lambda function.

**Private databases**: Vapor places serverless databases, and optionally fixed-size databases, in a private network. The Lambda functions must then run in that VPC. Copy the subnet IDs and the security group of the Vapor Lambda function (in the Lambda console, "Configuration" then "VPC") into `serverless.yml`:

```yml filename="serverless.yml"
provider:
    vpc:
        securityGroupIds:
            - sg-0123456789abcdef0
        subnetIds:
            - subnet-0123456789abcdef0
            - subnet-abcdef0123456789
```

This reuses the Vapor network, including its NAT gateway, so the functions keep their internet access. [Read more about VPC databases](https://bref.sh/docs/environment/database#vpc-databases-private-network).

**Without Bref Cloud**: the configuration is identical, only the database will not appear in a dashboard.

Once deployed, run the migrations as you did with the Vapor `deploy` hook:

```bash
bref command "migrate --force"
```

> [!NOTE]
>
> Vapor calls `Schema::defaultStringLength(191)` when the database is Aurora MySQL 5.7 (`VAPOR_SERVERLESS_DB=true`). If your application relies on it, add that line to the `boot()` method of `AppServiceProvider`.

## Cache and sessions

**Redis**: if the Vapor environment has a Redis cache (ElastiCache), keep it. It lives in the Vapor network, so the `provider.vpc` configuration above is required. Vapor configured the Redis connection automatically (`VAPOR_CACHE=true`), you now have to do it in `config/database.php`. Vapor caches are Redis clusters, so declare it as a cluster:

```php filename="config/database.php"
'redis' => [
    'client' => 'phpredis',
    'options' => [
        'cluster' => 'redis',
    ],
    'clusters' => [
        'default' => [
            [
                'host' => env('REDIS_HOST'),
                'port' => 6379,
                'database' => 0,
            ],
        ],
        'cache' => [
            [
                'host' => env('REDIS_HOST'),
                'port' => 6379,
                'database' => 0,
            ],
        ],
    ],
],
```

Set `REDIS_HOST` to the value injected by Vapor (in the Lambda console). The `redis` PHP extension is [enabled by default](https://bref.sh/docs/environment/php#extensions) in Bref.

**DynamoDB**: when no Redis cache is attached, Vapor uses a DynamoDB table as the default cache store. Cache data is disposable, so the simplest is to deploy a new table with `serverless.yml`. Follow the [Laravel caching guide](https://bref.sh/docs/laravel/caching). The database cache driver is also a good option: it's simple, fast, and avoids opening another HTTPS connection to DynamoDB.

**Sessions**: to keep users logged in through the migration, keep the same `SESSION_DRIVER` and the same session store (database, Redis or cookie). With the same `APP_KEY`, existing sessions remain valid.

If sessions are stored in DynamoDB (`SESSION_DRIVER=dynamodb`), they live in the Vapor DynamoDB table. A new table logs out all users. To avoid it, keep the Vapor table for sessions: keep the `DYNAMODB_CACHE_TABLE` variable and allow the functions to access the table:

```yml filename="serverless.yml"
provider:
    environment:
        SESSION_DRIVER: dynamodb
        DYNAMODB_CACHE_TABLE: vapor_cache # the value from .env.production
    iam:
        role:
            statements:
                -   Effect: Allow
                    Action:
                        - dynamodb:GetItem
                        - dynamodb:BatchGetItem
                        - dynamodb:PutItem
                        - dynamodb:UpdateItem
                        - dynamodb:DeleteItem
                    Resource: arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/vapor_cache
```

You can move sessions to another store later, when logging out users is acceptable.

**Failed jobs**: Vapor tracks job attempts in the cache. Bref uses Laravel's native `failed_jobs` table, make sure it exists (it is part of the default Laravel migrations).

## Queues

Vapor creates SQS queues for each environment and runs jobs in a queue Lambda function. Bref does the same with the Lift `queue` construct and the `Bref\LaravelBridge\Queue\QueueHandler` worker. Follow the [Laravel Queues guide](https://bref.sh/docs/laravel/queues):

```yml filename="serverless.yml"
provider:
    environment:
        QUEUE_CONNECTION: sqs
        SQS_QUEUE: ${construct:jobs.queueUrl}

constructs:
    jobs:
        type: queue
        worker:
            handler: Bref\LaravelBridge\Queue\QueueHandler
            runtime: php-84
            memorySize: 1024 # vapor.yml `queue-memory`
            timeout: 60 # vapor.yml `queue-timeout`
        maxConcurrency: 50 # vapor.yml `queue-concurrency`
```

Declare one construct per queue listed in `vapor.yml`, and `fifo: true` for FIFO queues ([Lift queue documentation](https://github.com/getlift/lift/blob/master/docs/queue.md)).

All Laravel Queues features work as usual: retries, `failed_jobs`, `queue:retry`, etc. The `vapor:work` command and the Vapor worker are no longer involved. Laravel Horizon is not applicable on Lambda, same as on Vapor.

**During the switch**: jobs already in the Vapor queues are processed by the Vapor environment as long as it exists. Wait for the Vapor queues to be empty (in the SQS console) before deleting the Vapor environment.

## Scheduler

`scheduler: true` in `vapor.yml` runs `php artisan schedule:run` every minute. In `serverless.yml`, this is a `schedule` event on the `artisan` function:

```yml filename="serverless.yml"
functions:
    artisan:
        handler: artisan
        runtime: php-84-console
        timeout: 720
        events:
            - schedule:
                  rate: rate(1 minute)
                  input: '"schedule:run"'
```

Like on Vapor, the scheduler is invoked once per minute, from a single place, so `onOneServer()` is not needed. A task that runs longer than a minute can still overlap with the next run: use `withoutOverlapping()` to prevent it. Sub-minute tasks work as long as the function `timeout` is longer than 60 seconds: `schedule:run` keeps running until the end of the minute.

> [!WARNING]
>
> While the Vapor environment exists, its scheduler runs every minute. If the Bref scheduler also runs, every scheduled task runs twice (emails, billing, reports…).

Deploy the Bref application without the `schedule` event first. When you switch the domain to Bref:

1. Set `scheduler: false` in `vapor.yml` and run `vapor deploy production`. Do this before you switch the DNS record, see [Domain and DNS switch](#domain-and-dns-switch).
2. Add the `schedule` event to the `artisan` function and run `bref deploy`.

Scheduled tasks do not run between the two deployments. Do it at a time when no important task is scheduled.

## File storage

Keep the S3 bucket configured with `storage` in `vapor.yml`. It is a regular S3 bucket in your AWS account: the files, URLs and lifecycle rules remain unchanged.

Configure Laravel to use it and grant the Lambda functions access to it:

```yml filename="serverless.yml"
provider:
    environment:
        FILESYSTEM_DISK: s3
        AWS_BUCKET: my-app-storage
    iam:
        role:
            statements:
                -   Effect: Allow
                    Action: s3:*
                    Resource:
                        - arn:aws:s3:::my-app-storage
                        - arn:aws:s3:::my-app-storage/*
```

This replaces the `storage` construct of the [File storage guide](https://bref.sh/docs/laravel/file-storage), which creates a new bucket. AWS credentials are provided by Lambda, there is no `AWS_ACCESS_KEY_ID` to set.

### File uploads

Vapor provides the `Vapor.store()` JavaScript helper to upload files from the browser to S3, because Lambda limits request payloads to about 4.5MB. It calls the `/vapor/signed-storage-url` route (provided by `laravel/vapor-core` and protected by the `uploadFiles` gate), uploads the file to a `tmp/` key in the bucket, and your code then moves the file with `Storage::copy()`.

Bref has the same limit and the same solution, built into `bref/laravel-bridge` ([Large files](https://bref.sh/docs/laravel/file-storage#large-files)):

- **Keep the `uploadFiles` gate** (or `UserPolicy::uploadFiles()`): the Bref route `/signed-upload-url` uses the same ability.
- **Replace `Vapor.store()`** with the `store()` function of the `bref-upload.js` helper shipped by the package (import it from `vendor/` with a Vite alias or publish it with `php artisan vendor:publish --tag=bref-upload`, see [Large files](https://bref.sh/docs/laravel/file-storage#large-files)):

  ```js
  import { store } from 'bref-upload';

  // Same signature and same `progress` option as Vapor.store()
  const { uuid, key, bucket, extension } = await store(file, {
      progress: (ratio) => console.log(`${Math.round(ratio * 100)}%`),
  });
  ```

  The response contains the same `uuid`, `key`, `bucket`, `url` and `headers` fields as Vapor's, plus `extension`. Keys are `tmp/{user id}/{uuid}.{extension}` instead of `tmp/{uuid}`. The `httpClient` option is supported too, to keep using your configured axios instance; `bucket`, `expires`, `visibility` and `data` are not.
- **Validate the key** sent by the browser with the `UploadedToS3` rule (it checks that the file was uploaded by the current user, exists, and matches the expected extension and size), then copy it to its final location with `Storage::copy()`, exactly like with Vapor.

Two things to check on the existing bucket:

- **CORS**: Vapor configured CORS for your domain. To test uploads on the temporary URL generated by Bref, add that origin to the CORS configuration of the bucket in the S3 console.
- **Lifecycle**: Vapor configured the `tmp/` prefix to expire after 24 hours, this rule stays in place.

## Assets

Vapor uploads the `public/` directory to a separate S3 bucket, serves it through CloudFront and injects `ASSET_URL`. Bref uses the [`server-side-website` construct](https://bref.sh/docs/use-cases/websites) instead: assets are uploaded to S3 and served by CloudFront under **the same domain as the application**. This removes the need for `ASSET_URL`:

- Remove `ASSET_URL` and `MIX_URL` from the environment variables.
- Remove `Vapor.asset()` and `Vapor.withBaseAssetUrl()` calls from your JavaScript, and the `ASSET_URL` or `VITE_VAPOR_ASSET_URL` configuration from `vite.config.js`. The `asset()` helper in Blade keeps working, with URLs on the application domain.
- The `serve_assets` and `redirect_robots_txt` options of `config/vapor.php` are replaced by the `assets` list of the construct: list the files and directories that CloudFront should serve from S3 (`public/build`, `favicon.ico`, `robots.txt`, etc.). Anything not listed is handled by Laravel.
- Build assets (`npm run build`) before running `bref deploy`, as you did in the Vapor `build` hook.

## Domain and DNS switch

Vapor manages domains with Route 53 hosted zones and ACM certificates created in your AWS account. Both can be reused.

1. Deploy with Bref **without** the `domain` option first, and test the application on the URL displayed by `bref deploy`.
2. Find the HTTPS certificate in the [ACM console](https://console.aws.amazon.com/acm/home?region=us-east-1#/certificates/list): CloudFront requires a certificate in the `us-east-1` region. Vapor certificates for API Gateway v1 are usually in `us-east-1`, otherwise [request a new one](https://bref.sh/docs/use-cases/websites#custom-domain-name), which is free.
3. Check if the Vapor domain is edge-optimized: in the [API Gateway console](https://console.aws.amazon.com/apigateway/main/publish/domain-names), open "Custom domain names" and look at the endpoint type of `example.com`. An edge-optimized domain uses a CloudFront distribution managed by AWS, and CloudFront allows a domain on only one distribution. The Bref deployment then fails with a `CNAMEAlreadyExists` error. In that case, the switch causes a short downtime (a few minutes), do it at a quiet time:
   - Delete the `example.com` custom domain in the API Gateway console. The Vapor application is no longer reachable on `example.com`.
   - Continue immediately with the next steps.

   A regional custom domain does not have this problem, the switch has no downtime.
4. Add the domain and the certificate ARN to the `website` construct and deploy again:

   ```yml filename="serverless.yml"
   constructs:
       website:
           type: server-side-website
           # ...
           domain: example.com
           certificate: arn:aws:acm:us-east-1:123456789012:certificate/...
   ```

5. Point the DNS record of `example.com` to the CloudFront domain of the `website` construct (displayed in the outputs of the deployment, or in the CloudFront console). If Vapor manages the zone, edit the record in the [Route 53 console](https://console.aws.amazon.com/route53/) as an alias to the CloudFront distribution, and do not deploy the Vapor environment again. If you manage DNS elsewhere, update the CNAME.

The `redirect_to_root` option of `config/vapor.php` becomes `redirectToMainDomain: true` on the construct, with both domains listed ([Lift documentation](https://github.com/getlift/lift/blob/master/docs/server-side-website.md#custom-domain)).

## Monitoring, logs and commands

| Vapor | Bref Cloud |
|---|---|
| `vapor deploy production` | `bref deploy --env=production` |
| `vapor env:pull`, `vapor env:push`, `vapor secret` | `serverless.yml` and `bref secret:create` ([Environment variables](#environment-variables-and-secrets)) |
| `vapor command production "migrate"` | `bref command "migrate" --env=production` ([Console runtime](https://bref.sh/docs/runtimes/console)) |
| `vapor tinker production` | `bref tinker --env=production` |
| `vapor down` and `vapor up` | `MAINTENANCE_MODE` environment variable ([Maintenance mode](https://bref.sh/docs/laravel/maintenance-mode)) |
| `vapor rollback` | Deploy the previous commit. Failed deployments are rolled back automatically by CloudFormation |
| Vapor dashboard and Vapor UI (logs, metrics, failed jobs) | Bref Cloud dashboard: logs, metrics, traces, queues and failed jobs ([Monitoring](https://bref.sh/docs/monitoring)) |
| `vapor database:shell`, `vapor jump`, `vapor cache:tunnel` | [7777](https://port7777.com), an SSH tunnel to private databases made by Bref maintainers |
| `vapor env:delete` | `bref remove --env=production` |

**Without Bref Cloud**: `serverless deploy`, `serverless bref:cli --args="migrate"`, CloudWatch logs and the [Bref Dashboard](https://dashboard.bref.sh/) ([Monitoring](https://bref.sh/docs/monitoring)).

## Clean up

Once the domain points to Bref and the Vapor queues are empty, you are free to delete the Vapor environment. Make sure not to delete resources that are still in use (like the database, buckets, etc.).
