# Bref documentation
> Bref is an open-source framework to run PHP applications on AWS Lambda (serverless). This file contains the whole documentation of https://bref.sh. Each page starts with its canonical URL.
---
Source: https://bref.sh/docs
# What is Bref and serverless?
Serverless means using cloud services that manage the servers for us.
## Why serverless?
When running PHP on a server, we must:
- set up, configure, and maintain that server,
- pay a fixed price for the server,
- scale the server(s) if we get more traffic.
When running PHP serverless:
- We do not need to set up servers, the cloud provider takes care of that.
- We pay only for what we use (per request).
- Our application scales automatically.
**Serverless provides more scalable, affordable, and reliable architectures with less effort.**
Serverless includes services like storage as a service, database as a service, message queue as a service, etc. One service in particular is interesting for us developers: *Function as a Service* (FaaS).
FaaS is a way to run code where the hosting provider takes care of setting up everything, keeping the application available 24/7, scaling it up and down, and we are only charged *while the code is actually executing*.
## Why Bref?
Bref aims to make running PHP applications simple.
To reach that goal, Bref takes advantage of serverless technologies. However, while serverless is promising, there are many choices to make, tools to build, and best practices to figure out.
Bref provides extensive documentation and an entire toolkit to make serverless approachable and easy to use.
### What is Bref
Bref (which means "brief" in French) comes as an open source Composer package and helps you deploy PHP applications to [AWS](https://aws.amazon.com) and run them on [AWS Lambda](https://aws.amazon.com/lambda/). It also makes it easy to use other AWS services (like S3, RDS, SQS…) for file storage, databases, etc.
Bref provides:
- documentation
- PHP runtimes for AWS Lambda
- deployment tooling
- integrations for Laravel and Symfony
The choice of AWS is deliberate: at the moment, AWS is the leading hosting provider, it is ahead in the serverless space in terms of features, performance, and reliability. AWS combines the advantages of being an extremely safe choice for hosting while providing the most advanced serverless solution.
Bref configures and deploys applications to AWS using [the `serverless` CLI](https://github.com/oss-serverless/osls). Being the most popular tool, `serverless` comes with a huge community, a lot of examples online, and a simple configuration format.
If you want to learn [how AWS Lambda and Bref work, read more here](https://bref.sh/docs/how-it-works).
## Use cases
Bref and AWS Lambda can be used to run many kinds of PHP applications, for example:
- APIs
- websites
- workers
- batch processes/scripts
- event-driven microservices
Bref aims to support any PHP framework. It comes with deep integrations with Laravel and Symfony.
If you are interested in real-world examples as well as cost analyses, head over to the [**Case Studies** page](https://bref.sh/docs/case-studies).
## Adapting to serverless
> Do PHP applications need to be adapted to run serverless?
In general no, but it depends on where you're coming from.
Running PHP applications serverless comes with roughly the same constraints as running an application in auto-scaled containers.
- The application code is mounted as read-only on disk.
- Since the application scales horizontally to run in multiple "instances" (like containers), all these instances have independent and ephemeral filesystems.
- Logs must not be written to disk, instead they must be sent to a centralized system (on AWS Lambda, logs written to `stderr` are automatically centralized in AWS CloudWatch, so this is usually a one-line config change).
- Sessions must not be written to disk, instead they must be stored in a centralized system (e.g. the database or Redis).
- Database and cache services (e.g. MySQL and Redis) must not run on the same server/container as PHP; instead, they must run in separate servers or containers (e.g. run databases in AWS RDS, Redis in AWS ElastiCache…).
- Uploaded files and generated files (e.g. CSV exports, PDF files…) must be stored in a centralized system (e.g. AWS S3).
- You can use the filesystem as a cache, but each instance of the application will then have a separate cache since the filesystem is not shared.
If these sound familiar to you, good news: your application is ready for serverless.
If your application is currently not set up like this, good news: preparing for serverless means you are actually preparing for a horizontally scalable application. These points are not "serverless-specific".
### Serverless-specific constraints
There are still a few serverless-specific constraints to be aware of:
- About 0.2% of all requests in production are ["cold starts"](https://bref.sh/docs/environment/performances#cold-starts), i.e. slower requests (AWS Lambda scaling up) that can add 200 ms to 500 ms (or even more on very large applications) to the HTTP response time.
If your application cannot tolerate that occasional latency at the p99 metric, then serverless might not be the best fit.
- AWS Lambda has a limit of ~4 MB for HTTP requests/uploaded files.
If your application needs to support uploading larger files, a better option is to update your JavaScript frontend to upload files directly to AWS S3 via S3 pre-signed URLs (Laravel has helpers to generate these URLs for example).
- We do not run long-running processes on AWS Lambda, like queue workers or websocket servers.
For queue workers, this is actually great: SQS and Lambda integrate natively so that we don't have to run workers (or even think about them). Bref does the rest of the job to integrate natively with Laravel Queues or Symfony Messenger. More on this in the rest of the Bref documentation.
For websocket servers, this means we cannot run servers like Ratchet or Laravel Reverb on Lambda. It is possible to run this on the side in an AWS EC2 server, or in a container in ECS, but this is extra setup. An alternative is to use the native API Gateway WebSocket feature, see the [WebSockets guide](https://bref.sh/docs/use-cases/websockets).
- AWS Lambda has a maximum execution time limit of 15 minutes *per invocation*.
Note that this limit applies "per invocation", i.e. per HTTP request, per SQS job, per CLI command, per cron task, etc. That does mean you cannot have one job or a PHP script running for more than 15 minutes. If you do, you can either try to split these tasks into smaller jobs, or run longer tasks in EC2 or ECS separately.
## Maturity matrix
The matrix below provides an overview of the "maturity level" for common PHP applications.
This maturity level is a vague metric, however it can be useful to anticipate the effort and the limitations to expect for each scenario. While a green note doesn't mean that Bref and Lambda are silver bullets for the use case (there are no silver bullets), a red note doesn't mean this is impossible or advised against.
This matrix will be updated as Bref and AWS services evolve over time.
Simplicity
Performance
Reliability
Jobs, Cron
APIs
Websites
Legacy applications
Event-driven microservices
WebSockets
Real-time applications
Is this documented and simple to achieve?
Is performance acceptable?
Is this scenario production-ready?
Legend:
Good use caseSome drawbacksStrong limitations
### Extensions installed but disabled by default
The following extensions are installed in Bref runtimes, but disabled by default:
- **[intl](https://www.php.net/manual/en/book.intl.php)** - Internationalization extension (referred to as Intl) is a wrapper for ICU library, enabling PHP programmers to perform various locale-aware operations.
- **[APCu](https://www.php.net/manual/en/book.apcu.php)** - APCu is APC stripped of opcode caching.
- **[Redis](https://github.com/phpredis/phpredis)** - A PHP extension for interfacing with Redis.
- **[soap](https://www.php.net/manual/en/book.soap.php)** - SOAP client and server for PHP
You can enable these extensions by loading them in `php/conf.d/php.ini` (as mentioned in [the section above](#phpini)), for example:
```ini filename="php/conf.d/php.ini"
extension=intl
extension=apcu
extension=redis
extension=soap
```
### Extra extensions
Due to space limitations in AWS Lambda, Bref runtimes cannot include every possible PHP extension. These additional PHP extensions are provided by the [brefphp/extra-php-extensions](https://github.com/brefphp/extra-php-extensions) project, as separate AWS Lambda layers and Docker images.
The most popular extra extensions are:
- **[gd](https://github.com/brefphp/extra-php-extensions/tree/master/layers/gd)** - image processing with the GD library
- **[imagick](https://github.com/brefphp/extra-php-extensions/tree/master/layers/imagick)** - image manipulation with ImageMagick
- **[gmp](https://github.com/brefphp/extra-php-extensions/tree/master/layers/gmp)** - GNU Multiple Precision arithmetic
- **[mongodb](https://github.com/brefphp/extra-php-extensions/tree/master/layers/mongodb)** - MongoDB driver
- **[sqlsrv](https://github.com/brefphp/extra-php-extensions/tree/master/layers/sqlsrv)** - Microsoft SQL Server driver (pdo_sqlsrv)
- **[memcached](https://github.com/brefphp/extra-php-extensions/tree/master/layers/memcached)** - Memcached client
- **[rdkafka](https://github.com/brefphp/extra-php-extensions/tree/master/layers/rdkafka)** - Apache Kafka client (librdkafka)
- **[amqp](https://github.com/brefphp/extra-php-extensions/tree/master/layers/amqp)** - RabbitMQ and AMQP client
- **[grpc](https://github.com/brefphp/extra-php-extensions/tree/master/layers/grpc)** and **[protobuf](https://github.com/brefphp/extra-php-extensions/tree/master/layers/protobuf)** - gRPC and Protocol Buffers
- **[ldap](https://github.com/brefphp/extra-php-extensions/tree/master/layers/ldap)** - LDAP directory access
- **[imap](https://github.com/brefphp/extra-php-extensions/tree/master/layers/imap)** - IMAP mail access
- **[xdebug](https://github.com/brefphp/extra-php-extensions/tree/master/layers/xdebug)** - debugging and profiling
- **[pcov](https://github.com/brefphp/extra-php-extensions/tree/master/layers/pcov)** - code coverage
- **[blackfire](https://github.com/brefphp/extra-php-extensions/tree/master/layers/blackfire)**, **[tideways](https://github.com/brefphp/extra-php-extensions/tree/master/layers/tideways)**, **[datadog](https://github.com/brefphp/extra-php-extensions/tree/master/layers/datadog)**, **[newrelic](https://github.com/brefphp/extra-php-extensions/tree/master/layers/newrelic)** - APM and profiling agents
And many more: [see the full list of extra extensions](https://github.com/brefphp/extra-php-extensions#available-layers). Contributions to add more PHP extensions are welcome.
> [!NOTE]
>
> Bref extra extensions are contributed by the community. Feel free to contribute to the repository or get in touch for [Bref support](https://bref.sh/support) if you want to ensure a specific extension is available and maintained.
#### Using extra extensions with Lambda layers
Install the `bref/extra-php-extensions` package, add its plugin to `serverless.yml` and reference the layer of each extension you need:
```bash
composer require bref/extra-php-extensions
```
```yml filename="serverless.yml" {3,10-11}
plugins:
- ./vendor/bref/bref
- ./vendor/bref/extra-php-extensions
functions:
api:
handler: public/index.php
runtime: php-84-fpm
layers:
- ${bref-extra:gd-php-84}
- ${bref-extra:imagick-php-84}
```
Keep in mind that AWS Lambda allows at most 5 layers per function, and that extra extensions are not available for ARM (`arm64`) yet.
#### Using extra extensions with Docker
If you [deploy using container images](https://bref.sh/docs/deploy/docker), each extra extension is also available as a Docker image (`bref/extra-gd-php-84`, `bref/extra-imagick-php-84`, etc.) that you can copy into your own image. [Read the "Extra PHP extensions" section of the Docker guide](https://bref.sh/docs/deploy/docker#extra-php-extensions).
### Custom extensions
It is also possible to provide your own extensions via [custom AWS Lambda layers](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html).
> This guide is really raw, feel free to contribute to improve it.
To create your custom layer, you will need to:
- compile the extension (and any required libraries) in the same environment as AWS Lambda and Bref
- include the compiled extension (and required libraries) in a layer
- upload the layer to AWS Lambda
- include the layer in your project
- enable the extension in a custom `php.ini`
To compile the extension, Bref provides the `bref/build-php-*` Docker images. Here is an example with Blackfire:
```dockerfile
FROM bref/build-php-84:3
RUN curl -A "Docker" -o /tmp/blackfire.so -L -s "https://packages.blackfire.io/binaries/blackfire-php/1.42.0/blackfire-php-linux_amd64-php-84.so"
# Build the final image from the amazon image that is close to the production environment
FROM public.ecr.aws/lambda/provided:al2023
# Copy things we installed to the final image
COPY --from=0 /tmp/blackfire.so /opt/bref-extra/blackfire.so
```
The `.so` extension file can then be retrieved in `/opt/bref-extra/blackfire.so`.
If you installed system libraries, you may also need to copy them to the `public.ecr.aws/lambda/provided:al2023`
image.
See [brefphp/extra-php-extensions](https://github.com/brefphp/extra-php-extensions)
for more examples.
## Custom vendor path
Bref automatically requires vendor dependencies from the default `vendor/autoload.php` path.
If your Composer dependencies are installed elsewhere, you can customize that path via the `BREF_AUTOLOAD_PATH` environment variable.
```yml filename="serverless.yml"
provider:
# ...
environment:
BREF_AUTOLOAD_PATH: '/var/task/foo-bar/vendor/autoload.php'
```
The path must start with `/var/task`, which is the directory where projects are installed on AWS Lambda.
---
Source: https://bref.sh/docs/environment/storage
# Storage on AWS Lambda
Here is a simplified overview of the filesystem on AWS Lambda:
```bash
/opt/
# Where Lambda runtimes and layers (like Bref) are unzipped
/var/task/
# Where your application code is unzipped
/tmp/
# Temporary files
...
```
The `/var/task` directory is [limited to 250MB](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html#function-configuration-deployment-and-execution). If you hit that limit, you can deploy [via Docker images instead](https://bref.sh/docs/deploy/docker).
The filesystem on AWS Lambda **is read-only**, except for the `/tmp` directory.
On top of that, the filesystem is not shared between instances of a lambda when it scales up. For example a file `/tmp/foo.json` will not be shared across instances of the same lambda. Since a lambda can scale up or down at any time, data in the `/tmp` directory can be lost.
## Application data
Application data **must not** be stored in `/tmp` because of the behavior described above.
Instead, data can be stored in [databases](https://bref.sh/docs/environment/database) or in storage services like AWS S3.
### S3 storage
It is possible to deploy an S3 bucket in `serverless.yml` using the [`Storage` feature of the Lift plugin](https://github.com/getlift/lift/blob/master/docs/storage.md). For example:
```yml filename="serverless.yml"
provider:
environment:
BUCKET_NAME: ${construct:reports-bucket.bucketName}
constructs:
reports-bucket:
type: storage
allowAcl: true
```
The [`allowAcl: true` configuration](https://github.com/getlift/lift/blob/master/docs/storage.md#acl-support) is needed because S3 buckets have ACLs disabled by default since April 2023. Many tools and libraries (including PHP's Flysystem, used by Laravel) send ACL headers on S3 operations, which will fail on buckets with ACLs disabled. The `allowAcl: true` setting lets the bucket accept these headers without errors. Note that files in the bucket are still completely private, there is no change in the security of the bucket.
Read more [in the Lift documentation](https://github.com/getlift/lift/blob/master/docs/storage.md). If you use Laravel, check out the [Laravel file storage documentation](https://bref.sh/docs/laravel/file-storage) for a complete guide including presigned uploads, CORS configuration, and common pitfalls.
## Application cache
Performance-wise, using AWS S3 for storing the application cache is not ideal.
The following solutions can be used instead:
- pre-generate the cache in the project directory before deploying
- store the cache into the `/tmp` directory
- store the cache into a distributed cache service like Memcache, Redis or DynamoDB (an AWS service which fits the pay-per-request model nicely)
### Pre-generating the cache
Some frameworks allow pre-generating some caches. For example in Symfony the container can be compiled via `bin/console cache:warmup`, or in Laravel the config cache can be generated before deploying.
When possible **this is the best solution**: no generation will occur in production, and reading from the filesystem will be fast.
### Store in the `/tmp` directory
Some framework or library caches must be written into files. In that case storing in the `/tmp` directory is a good solution.
Remember that anything stored in `/tmp` will be lost when a lambda stops. When a lambda starts, the `/tmp` directory will be empty so the cache will be generated again.
Note that this is useful for deployments: no need to clear caches on deployments since a new version of the lambda will run on new instances (with an empty `/tmp` directory).
This solution is ideal when the cached data is fast to generate and never changes (e.g. template caching, framework caches).
### Store in a distributed cache service
Using a distributed cache service has the following advantages:
- the cache is not lost when the lambda scales down
- the cache is not lost when deploying
- the cache is shared between all lambda instances
The disadvantage is that if the data format changes between deployments, then a deployment strategy must be used to either clear the cache and regenerate it, or separate the cache between application versions.
Cache services that can be used include for example Redis, Memcache or DynamoDB. AWS offers those as managed services through AWS ElastiCache (Redis, Memcache) or DynamoDB.
Note that Redis and Memcache (through ElastiCache) run even when not used, which incurs costs. DynamoDB is a little slower than both of those but can be deployed in a "pay-per-request" mode where costs are proportional to the usage. There is a package implementing the PSR cache interfaces using DynamoDB ([rikudou/psr6-dynamo-db](https://github.com/RikudouSage/DynamoDbCachePsr6)).
This solution is ideal for cache data that can change during the life of the application (e.g. caching a website menu, an API response…).
#### Deploying DynamoDB tables
Like any service, DynamoDB tables can be deployed via CloudFormation using the `resources` key in `serverless.yml`:
```yml filename="serverless.yml"
service: app
...
resources:
Resources:
CacheTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions: # only keys are defined here, other attributes are dynamic
- AttributeName: id # adds a mandatory id field
AttributeType: S # the type of id is a string
BillingMode: PAY_PER_REQUEST # billed for each request instead of paying for a constant capacity
TimeToLiveSpecification: # deletes cache keys automatically based on a ttl field which contains a timestamp
AttributeName: ttl
Enabled: true
KeySchema:
- AttributeName: id
KeyType: HASH # the type of key, HASH means partition key (similar to primary keys in SQL)
```
We need to allow code in Lambda functions to access DynamoDB.
We can also pass the table name as an environment variable to the application.
```yml filename="serverless.yml"
service: app
provider:
iam:
role:
statements:
- Effect: Allow
Resource: !GetAtt CacheTable.Arn
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
environment:
# This environment variable will contain the table name
DYNAMODB_CACHE_TABLE: !Ref CacheTable
```
---
Source: https://bref.sh/docs/environment/logs
# Logs
As explained in the [storage documentation](https://bref.sh/docs/environment/storage), the filesystem on AWS Lambda is:
- read-only, except for `/tmp`
- not shared between lambda instances
- not persistent
Because of that, logs should not be stored on disk.
## CloudWatch
Instead of storing logs on disk, logs should be pushed to [AWS CloudWatch](https://aws.amazon.com/cloudwatch/), AWS' service for logs.
### Writing logs
By default, Bref will forward low-level PHP errors and warnings to CloudWatch.
For all other logs, your application should write logs to CloudWatch:
- [With the PHP-FPM runtime for web apps](https://bref.sh/docs/runtimes/fpm-runtime): write logs to `stderr`
- [With the runtime for event-driven functions](https://bref.sh/docs/runtimes/function): write logs to `stdout` (using `echo` for example) or `stderr`
AWS Lambda has a built-in mechanism to forward logs written to `stderr` (or `stdout` for event-driven functions) to CloudWatch Logs in the background, **without performance impact**.
If you use Laravel, Bref will automatically configure Laravel to log to CloudWatch via `stderr` (`LOG_CHANNEL=stderr`) with the `Bref\Monolog\CloudWatchFormatter` formatter. You don't have to do anything.
If you have a custom log setup (e.g. using the `stack` channel), you should ensure that the `stderr` channel is included in your stack with the `Bref\Monolog\CloudWatchFormatter` formatter.
With this formatter, logs will contain structured data that can be filtered in CloudWatch Logs Insights. For example, you can filter by log level or exception class.
If you use Symfony, Bref will automatically configure Symfony to log to CloudWatch via `stderr` with the `Bref\Monolog\CloudWatchFormatter` formatter. You don't have to do anything.
This is done by the `Bref\SymfonyBridge\BrefSymfonyBridgeBundle` bundle, which Symfony Flex adds to `config/bundles.php` when installing `bref/symfony-bridge`. If it is missing from that file, add it:
```php filename="config/bundles.php"
return [
// ...
Bref\SymfonyBridge\BrefSymfonyBridgeBundle::class => ['all' => true],
];
```
If you have a custom log setup, you should ensure that logs are sent to `stderr` (e.g. using the `stream` handler with `php://stderr`) with the `Bref\Monolog\CloudWatchFormatter` formatter (`bref.cloudwatch_formatter` service).
With this formatter, logs will contain structured data that can be filtered in CloudWatch Logs Insights. For example, you can filter by log level or exception class.
You can use [Monolog](https://github.com/Seldaek/monolog) to write logs to CloudWatch via `stderr`:
```php
$log = new Monolog\Logger('default');
$log->pushHandler(new StreamHandler('php://stderr', Logger::INFO));
$log->warning('This is a warning!');
```
Bref provides a formatter optimized for CloudWatch, it is highly recommended to use it:
```bash
composer require bref/monolog-bridge
```
```php
$log = new Monolog\Logger('default');
$handler = new StreamHandler('php://stderr', Logger::INFO);
$handler->setFormatter(new Bref\Monolog\CloudWatchFormatter);
$log->pushHandler($handler);
$log->warning('This is a warning!');
```
For simple needs, you can replace Monolog with [Bref's logger](https://github.com/brefphp/logger), a PSR-3 logger designed for AWS Lambda:
```php
$log = new \Bref\Logger\StderrLogger();
$log->warning('This is a warning!');
```
### Reading logs
You can view, search and tail logs in the [Bref Cloud](https://bref.sh/cloud) dashboard:
You can also search logs from the terminal with `bref logs`:
```bash
# Logs of the last hour
bref logs --env=prod
# Logs of the last day of the "web" function that contain "payment" and "failed"
bref logs --env=prod --since=1d --function=web --search="payment failed"
# Between two dates (UTC), with the stack traces of exceptions
bref logs --env=prod --since="2026-09-23 14:00" --until="2026-09-23 15:00" --full
```
`bref logs` shows one line per log entry. It hides the lines that Lambda and PHP-FPM write on every invocation (`START`, `END`, `REPORT`…), use `--all` to include them.
Run `bref logs --help` for all the options. Add `--json` for machine-readable output. To use the command outside the project directory, set the application with `--app` and `--team`. `bref logs` is also designed for [AI agents](https://bref.sh/docs/cloud#ai-agents).
You can view logs in the [CloudWatch console](https://console.aws.amazon.com/cloudwatch/home#logs:), or in the terminal with `serverless logs`:
```bash
serverless logs -f
# Tail logs:
serverless logs -f --tail
```
### Logs of a single request
Every Lambda invocation (an HTTP request, a queue job, a console command…) has a **request ID**. Lambda writes it in its `START`, `END` and `REPORT` lines, and the `Bref\Monolog\CloudWatchFormatter` formatter starts every log line with it:
```
8f507cfc-8b35-4e7e-9f26-f2a3a6e7e1a2 ERROR Payment failed {"message":"Payment failed","level":"ERROR",...}
```
Search for a request ID to see everything that happened during that invocation: the application logs, Bref's errors (for example a timeout), and Lambda's `REPORT` line with the duration and the memory used.
In the [Bref Cloud](https://bref.sh/cloud) dashboard, click the request ID next to a log line to see all the logs of that request. The [X-Ray trace](https://bref.sh/docs/monitoring#traces) of a request also shows its logs:
From the terminal, search for the request ID with `bref logs`. Its first 8 characters are enough, like in the dashboard:
```bash
bref logs --env=prod --search=8f507cfc --all
```
In CloudWatch Logs Insights, select the log group of the function and filter on the `@requestId` field:
```
fields @timestamp, @message
| filter @requestId = '8f507cfc-8b35-4e7e-9f26-f2a3a6e7e1a2'
| sort @timestamp asc
```
If you use another log formatter, the request ID of the current invocation is available in `$_SERVER['LAMBDA_REQUEST_ID']`. For example, to add it to every Monolog record:
```php
$logger->pushProcessor(function (Monolog\LogRecord $record) {
$record->extra['requestId'] = $_SERVER['LAMBDA_REQUEST_ID'] ?? null;
return $record;
});
```
---
Source: https://bref.sh/docs/environment/database
# Using a database
AWS offers the [RDS](https://aws.amazon.com/rds/) service to run MySQL and PostgreSQL databases.
Here are some of the database services offered by RDS:
- MySQL
- PostgreSQL
- [Aurora MySQL/PostgreSQL](https://aws.amazon.com/rds/aurora/): AWS-managed database compatible with MySQL or PostgreSQL
- [Aurora Serverless v2 MySQL/PostgreSQL](https://aws.amazon.com/rds/aurora/serverless/): similar to Aurora but scales automatically on-demand
> [!TIP]
>
> Aurora Serverless can be configured to scale down to 0 when unused (which costs $0), however be careful with this option: the database can take up to 15 seconds to un-pause.
AWS also offers [Aurora DSQL](https://bref.sh/docs/environment/database-dsql), a serverless database compatible with PostgreSQL: it needs no VPC, no password, and costs nothing while idle. Read [when to use it](https://bref.sh/docs/environment/database-dsql#when-to-use-aurora-dsql) rather than an RDS database.
## Internet-accessible vs VPC databases
| | Internet-accessible | VPC (private network) |
|---|---|---|
| Security | Password only | Network-isolated + password |
| Best for | Non-critical projects | Projects requiring strong isolation |
| Complexity | Simple | Requires VPC + NAT Gateway |
| Extra cost | None | ~$33/month for NAT Gateway |
| Access from your machine | Direct connection | Via SSH tunnel (e.g. [7777](https://port7777.com)) |
An internet-accessible database is the simplest option and works well for most projects. Use a VPC database when you need stronger network isolation.
## Internet-accessible databases
In the Bref Cloud dashboard, open the "Databases" page and [click "Create database"](https://bref.cloud/databases/create).
Bref Cloud can create classic RDS databases as well as serverless databases ([Aurora Serverless v2](https://aws.amazon.com/rds/aurora/serverless/)), in both MySQL and PostgreSQL flavors. Serverless databases scale automatically with traffic and can auto-pause when unused.
Fill in the form and click "Create":
The configuration with credentials securely stored in SSM will be displayed once the database is created.
In the [RDS console](https://console.aws.amazon.com/rds/home):
- switch to the region of your application
- click "Create database"
- select the type of database you want to create (engine, instance class, etc.) and fill the rest of the form
- make sure to select "Public access: Yes"
Once the database is created, make sure the security group allows inbound connections on the database port from any IP address (AWS Lambda IPs are dynamic).
Copy the endpoint (hostname) and configure your PHP application to connect to it. Don't forget to [securely store the username and password in AWS SSM](https://bref.sh/docs/environment/variables).
Tips to better control costs:
- for non-critical databases you can disable replication
- switch storage to "General Purpose (SSD)" for lower costs
- you can disable "enhanced monitoring" to avoid the associated costs
## VPC databases (private network)
Bref Cloud handles VPC creation, subnets, security groups, and NAT Gateway configuration for you.
**1. Create a network** in the Bref Cloud dashboard:
The network includes a VPC, subnets, security groups, and a NAT Gateway (~$33/month, shown in the dashboard). A single network can be reused across multiple applications and databases.
**2. Create a database** and select the network you created.
**3. Deploy** — Bref Cloud automatically adds the VPC configuration to your deployment. No changes to `serverless.yml` are needed.
> [!WARNING]
>
> Lambda functions in a VPC **lose internet access**. This can cause timeouts. You need a NAT Gateway to restore internet access (~$33/month). Alternatively, you can use [a NAT instance](https://fck-nat.dev/) (from $3/month).
You can follow [this tutorial](https://medium.com/@philippholly/aws-lambda-enable-outgoing-internet-access-within-vpc-8dd250e11e12) to set up a NAT Gateway, use [the serverless VPC plugin](https://github.com/smoketurner/serverless-vpc-plugin), or use the complete example in [Serverless Visually Explained](https://serverless-visually-explained.com/).
You can use one VPC and one NAT Gateway for multiple projects. You can also access some AWS services (S3, SQS, etc.) without a NAT Gateway via [private VPC endpoints](https://docs.aws.amazon.com/en_pv/vpc/latest/userguide/vpc-endpoints-access.html), but this does not replace a NAT Gateway for external APIs.
### Creating a database
In the [RDS console](https://console.aws.amazon.com/rds/home):
- click "Create database"
- select the type of database you want to create and fill in the form
- for a simpler configuration leave the default VPC in the last step
Tips to better control costs:
- for non-critical databases you can disable replication
- switch storage to "General Purpose (SSD)" for lower costs
- you can disable "enhanced monitoring" to avoid the associated costs
### Connecting Lambda to the VPC
To retrieve the information needed to let AWS Lambda access the database, go into [the RDS dashboard](https://console.aws.amazon.com/rds/home#databases:) and open the database you created.
> [!NOTE]
>
> It may take a few minutes for the database to be created.
Find:
- The **endpoint** (hostname of the database, available after creation completes).
- The **security group ID** (in the "VPC security groups" section), e.g. `sg-03f68e1100481622b`.
- The list of **subnets**, e.g. `subnet-12f4130e` (there are several, one per availability zone).
Add the VPC configuration to `serverless.yml` ([documentation](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/functions.md#vpc-configuration)):
```yml filename="serverless.yml"
functions:
hello:
# ...
vpc:
securityGroupIds:
- sg-03f68e1100481622b
subnetIds:
- subnet-12f4130e
- subnet-c5fe33e5
- subnet-11aa85dc
- subnet-85dcf240
```
### Authorizing Lambda connections
You need to allow Lambda to connect to the database's security group:
- open the database in RDS and click the security group
- in the "Inbound" tab click "Edit"
- add a rule: select MySQL/Aurora (or PostgreSQL) and set a "custom" source: select the security group itself (type `sg-` and use the autocompletion)
- save
Learn more in the AWS documentation about [configuring a Lambda to access resources in a VPC](https://docs.aws.amazon.com/lambda/latest/dg/vpc.html).
## Connecting from PHP
[Bref Cloud](https://bref.sh/cloud) automatically creates SSM parameters for your database credentials and provides the environment variables to set up.
If you are not using Bref Cloud, store credentials in SSM parameters ([read more](https://bref.sh/docs/environment/variables#secrets)) and configure the environment variables in `serverless.yml`:
```yml filename="serverless.yml"
provider:
environment:
DB_HOST:
DB_DATABASE: my_database
DB_USERNAME: ${ssm:/my-app/db-username}
DB_PASSWORD: ${ssm:/my-app/db-password}
```
Laravel reads these environment variables automatically to configure the database connection.
```yml filename="serverless.yml"
provider:
environment:
DATABASE_URL: mysql://${ssm:/my-app/db-username}:${ssm:/my-app/db-password}@/my_database
```
Doctrine is auto-configured by Symfony using the `DATABASE_URL` environment variable.
Connect to the database using the endpoint, for example with PDO:
```
mysql://user:password@dbname.e2sctvp0nqos.us-east-1.rds.amazonaws.com/dbname
```
Also refer to the [Extensions](https://bref.sh/docs/environment/php#extensions) section to see if you need to enable any database-specific extensions.
## Running database migrations
```bash
bref command "migrate --force"
```
```bash
serverless bref:cli --args="migrate --force"
```
```bash
bref command "doctrine:migrations:migrate --no-interaction"
```
```bash
serverless bref:cli --args="doctrine:migrations:migrate --no-interaction"
```
You can run any CLI script via the console runtime. Read the [Console runtime documentation](https://bref.sh/docs/runtimes/console) to learn more.
> [!TIP]
>
> A console function must be defined in your `serverless.yml`. Check the "Getting started" guide corresponding to your framework for an example.
## Accessing the database from your machine
**Internet-accessible databases** can be connected to directly from your machine using tools like TablePlus, DBeaver, or any database client.
**VPC databases** cannot be accessed from the internet directly. To connect from your machine, use an SSH tunnel.
To create an SSH tunnel easily and securely, **[check out 7777](https://port7777.com/?utm_source=bref)**, made by Bref maintainers:
To expose the database publicly on the internet instead, [follow this guide](https://bref.sh/docs/environment/database-public).
---
Source: https://bref.sh/docs/environment/database-dsql
# Using Aurora DSQL with Bref
[Aurora DSQL](https://aws.amazon.com/rds/aurora/dsql/) is a serverless database from AWS, compatible with PostgreSQL.
Compared to [RDS databases](https://bref.sh/docs/environment/database), including Aurora Serverless, Aurora DSQL is truly serverless:
- **No instance**: Aurora Serverless scales the capacity of a database instance up and down, while Aurora DSQL has no instance at all. It scales with the traffic, and down to zero without cold starts: a paused Aurora Serverless database takes up to 15 seconds to resume.
- **Billed per request**: Aurora DSQL is billed for the requests it serves, measured in "DPUs" (Distributed Processing Units), and for the data it stores. There are no DPU charges while the database is idle, only the storage is billed. RDS and Aurora Serverless instances are billed per hour while they run. The [AWS free tier](https://aws.amazon.com/rds/aurora/dsql/pricing/) includes 100,000 DPUs and 1 GB of storage per month.
- **Multi-AZ**: Aurora DSQL runs across three Availability Zones, with no failover to manage. RDS and Aurora Serverless databases run on an instance in a single Availability Zone, unless you pay for more instances in other zones to fail over to.
On AWS Lambda, it also requires:
- No VPC (virtual private network) and no NAT Gateway: Lambda functions connect to a public endpoint, over TLS.
- No password: applications authenticate with their IAM role.
## When to use Aurora DSQL
Aurora DSQL is a good fit for:
- **New applications**: their schema and code can take [the differences with PostgreSQL](#differences-with-postgresql) into account from the start.
- **Side projects and small applications**: with no fixed cost, an application with little traffic can run on AWS Lambda for $0, or a few cents per month.
- **Irregular traffic**: Aurora DSQL scales with the traffic, with no DPU charges when idle and no cold starts.
- **Applications that need high availability** without paying for standby instances.
Prefer [an RDS or Aurora Serverless database](https://bref.sh/docs/environment/database) for:
- **Existing applications** with a complex schema, or that rely on PostgreSQL features Aurora DSQL doesn't support: triggers, extensions (like PostGIS), full-text search, changing the type of columns, etc.
- **MySQL applications**: Aurora DSQL is only compatible with PostgreSQL.
- **Large data changes in one transaction**: imports, batch updates or cascading deletes that change more than 3,000 rows at once must be split into several transactions.
- **Many concurrent writes to the same rows**, like a global counter: with Aurora DSQL, [they conflict](#conflicts-between-requests) instead of waiting for each other.
- **Reporting and analytics**: Aurora DSQL is designed for transactional workloads (many short queries), not for heavy queries over large amounts of data.
> [!NOTE]
>
> Aurora DSQL is a recent service (generally available since 2025), and it evolves quickly: some of these limitations may be lifted in the future.
## Creating a database
In the Bref Cloud dashboard, open the "Databases" page, [click "Create database"](https://bref.cloud/databases/create) and select "Aurora DSQL". Create it in the same AWS account and region as your application.
Aurora DSQL has no snapshots: Bref Cloud backs up the database every day with [AWS Backup](https://aws.amazon.com/backup/), and keeps the backups 7 days by default (set 0 to disable daily backups). Every backup is a full copy of the data, billed per GB: the form estimates what the database will cost, backups included. Bref Cloud also takes a final backup when the database is deleted.
Once the database is created, its page shows its endpoint and the configuration to add to `serverless.yml`.
Create a cluster in the region of your application:
```bash
aws dsql create-cluster --region us-east-1 --deletion-protection-enabled
```
The command returns the identifier of the cluster and its endpoint: `.dsql..on.aws`.
Creating the cluster outside of `serverless.yml` keeps its data independent from the application: removing the application does not delete the database.
Aurora DSQL has no automatic backups: back up the cluster [with AWS Backup](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/backup-aurora-dsql.html).
## Differences with PostgreSQL
Aurora DSQL is compatible with PostgreSQL 16, with [some differences](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/working-with-postgresql-compatibility.html). The main ones are:
- **Concurrency**: when two transactions change the same rows at the same time, PostgreSQL makes the second one wait for the first one. Aurora DSQL lets both run, and the second one to commit fails with a conflict error (SQLSTATE `40001`). Its changes are rolled back, and the transaction must be retried. This also applies to queries run outside an explicit transaction: each query is a transaction on its own. The longer a transaction lasts, the more likely another transaction changes the same rows in the meantime. [The Laravel package handles retries automatically](#conflicts-between-requests).
- **Schema changes**: a transaction can contain a single schema change, and no data change. Indexes are built in the background (`CREATE INDEX ASYNC`). A column added to an existing table must be nullable, without a default value. The type of a column can't be changed, and a column can't become `NOT NULL` after the table is created. The primary key is declared when the table is created, and can't be changed afterwards. A foreign key added to an existing table doesn't check the rows that already exist (`NOT VALID`).
- **Auto-increment**: there are no `SERIAL` columns, identity columns (`BIGINT GENERATED BY DEFAULT AS IDENTITY`) replace them.
- **Limits**: a transaction can change up to 3,000 rows (including cascades) and 10 MiB of data, and last up to 5 minutes. A cluster accepts [at most 100 new connections per second](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/CHAP_quotas.html) (with bursts up to 1,000).
- **Collation**: strings are sorted by byte (collation `C`): accented letters come after `z`, and `lower()`/`upper()` only change ASCII letters.
- **Not supported**: savepoints, `TRUNCATE`, `SELECT … FOR SHARE`, full-text search, temporary tables, triggers, PL/pgSQL (functions can only be written in SQL), advisory locks and PostgreSQL extensions.
## Laravel
Install the [`bref/laravel-dsql`](https://github.com/brefphp/laravel-dsql) package:
```bash
composer require bref/laravel-dsql
```
It adds a `dsql` database connection to Laravel, which generates an IAM authentication token for each connection and adapts migrations to Aurora DSQL.
> [!WARNING]
>
> If your `composer.json` [removes unused AWS services](https://bref.sh/docs/deploy#reducing-package-size) from the AWS SDK, add `DSQL` to the services it keeps, then run `composer install`. The package uses it to generate the authentication tokens.
>
> ```json filename="composer.json"
> "extra": {
> "aws/aws-sdk-php": [
> "Sqs",
> "DSQL"
> ]
> }
> ```
Configure the connection, and allow the application to connect to the database, in `serverless.yml`:
```yml filename="serverless.yml"
provider:
environment:
DB_CONNECTION: dsql
DB_HOST: .dsql..on.aws
iam:
role:
statements:
- Effect: Allow
Action: dsql:DbConnectAdmin
Resource: arn:aws:dsql::${aws:accountId}:cluster/
```
Bref Cloud shows this configuration with the values of your database on the database page.
The package signs the IAM token with the AWS credentials of the environment: the IAM role of the Lambda function, or `AWS_PROFILE` when running locally. The token is signed locally, without any API call.
The application connects as the `admin` database role. To connect with [another role](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/using-database-and-iam-roles.html), set `DB_USERNAME` and allow the `dsql:DbConnect` action instead.
Deploy the application and [run the database migrations](https://bref.sh/docs/environment/database#running-database-migrations).
### Configuration
The package adds this `dsql` connection, unless `config/database.php` already defines one:
```php
'dsql' => [
'driver' => 'dsql',
'host' => env('DB_HOST'),
'port' => '5432',
'database' => 'postgres',
'username' => env('DB_USERNAME', 'admin'),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'verify-full',
'sslrootcert' => env('DB_SSLROOTCERT', 'system'),
],
```
To customize it, copy it into the `connections` array in `config/database.php`. The connection also accepts a `region`, for endpoints that don't include one, such as VPC endpoints.
### Connections and Octane
With PHP-FPM, Laravel opens a new database connection for every request. With Aurora DSQL, opening a connection (TLS and IAM authentication) adds latency to the request, and a cluster accepts [at most 100 new connections per second](https://docs.aws.amazon.com/aurora-dsql/latest/userguide/CHAP_quotas.html) (with bursts up to 1,000).
For applications with more than a few requests per second, run them with [Laravel Octane](https://bref.sh/docs/laravel/octane), and keep [the database connection open between requests](https://bref.sh/docs/laravel/octane#persistent-database-connections) with `OCTANE_PERSIST_DATABASE_SESSIONS`:
```yml filename="serverless.yml"
functions:
web:
handler: Bref\LaravelBridge\Http\OctaneHandler
runtime: php-84
environment:
BREF_LOOP_MAX: 250
OCTANE_PERSIST_DATABASE_SESSIONS: 1
events:
- httpApi: '*'
```
### Migrations
The package adapts Laravel migrations to [the differences between Aurora DSQL and PostgreSQL](#differences-with-postgresql):
- Migrations don't run in a transaction, since a transaction can only contain one schema change: each statement is committed on its own. A migration that fails halfway leaves the changes it made before.
- The primary key, unique keys and foreign keys of a new table are declared in its `create table` statement. A unique index added to an existing table enforces uniqueness once it is built in the background.
- Auto-incrementing columns (`$table->id()`) are `bigint` identity columns.
- To add a column with a default value to an existing table, add it as nullable first, then set its default in a second step. Rows that already exist keep `null`: fill them in batches, since a transaction can change at most 3,000 rows. Each batch runs in its own transaction, since migrations don't run in a transaction:
```php
Schema::table('posts', function (Blueprint $table) {
$table->string('status')->nullable();
});
Schema::table('posts', function (Blueprint $table) {
$table->string('status')->nullable()->default('draft')->change();
});
DB::table('posts')->whereNull('status')->chunkById(1000, function ($posts) {
DB::table('posts')->whereIn('id', $posts->pluck('id'))->update(['status' => 'draft']);
});
```
- `->change()` can make a column nullable, and change its default value and its comment. The type given to `->change()` is ignored.
The package throws an exception with an explanation when it encounters an unsupported schema change. Earlier changes in the migration may already have been committed.
### Conflicts between requests
With Aurora DSQL, two transactions that change the same rows at the same time conflict: the first one to commit wins, and the other one fails with an error. All changes in the failed transaction are rolled back. This can happen, for example, when two visitors buy the last item of a product at the same time.
**Queries outside of an explicit transaction**, such as the query issued by `$product->save()` or `Product::where(…)->update(…)`, are retried automatically by the package when they conflict, up to 3 times after the initial attempt. Since the conflict cancelled the query entirely, running it again doesn't apply it twice. If all retries fail, the exception is passed to your application.
**Queries inside a transaction** can't be run again on their own: the conflict cancels the whole transaction, including the queries that ran before, and your code may have made decisions based on data that another request just changed. The whole transaction has to run again. Laravel does it when you give `DB::transaction()` a number of attempts:
```php
DB::transaction(function () use ($productId) {
$product = Product::findOrFail($productId);
abort_if($product->stock === 0, 409, 'Out of stock');
$product->decrement('stock');
$product->orders()->create(['user_id' => auth()->id()]);
}, attempts: 3);
```
When the two visitors buy the last item at the same time, both transactions read a stock of 1 and decrement it, but only the first one commits. The second one runs again, reads a stock of 0, and the visitor gets the "Out of stock" error. Here, `attempts: 3` allows up to 3 attempts in total. Without it, a conflict throws an exception immediately; with it, an exception is still thrown if all attempts fail.
Keep in mind that:
- The closure can run several times: send emails, dispatch jobs or call APIs after the transaction, not inside it.
- The conflict is detected when the transaction commits, at the end of the closure, not on the query that changed the row.
- Rows that the transaction only reads, without changing them, are not checked: if another request changes them, the transaction still commits, based on the value it read. Read them with `lockForUpdate()` when your code relies on them: it doesn't make other requests wait like in PostgreSQL, but the transaction then conflicts if another request changes these rows.
- Many requests that change the same row, like a global counter, conflict often: spread the writes over several rows when you can.
**Transactions opened by Laravel or by packages** have a single attempt too, for example `saveOrFail()`, `deleteOrFail()`, or `syncOrFail()` and the other `…OrFail()` methods of relationships. When they can conflict, wrap the call in `DB::transaction()` with attempts: the inner transaction becomes part of the outer one, which runs again.
### Cache, locks and queues
The `database` cache store works with Aurora DSQL. Use [DynamoDB](https://bref.sh/docs/laravel/caching#dynamodb-cache) instead for a cache read on every request, for locks, or for rate limiters with a lot of traffic: when many requests change the same key at the same time, Aurora DSQL makes them conflict.
For queues, use [SQS](https://bref.sh/docs/laravel/queues) rather than the database queue.
### Other differences
- Without savepoints, a nested transaction is part of the transaction around it: rolling back the nested transaction rolls back nothing until the outer transaction is rolled back.
- `truncate()` and `sharedLock()` are not supported.
## Other PHP applications
AWS provides [a connector for PDO](https://github.com/awslabs/aurora-dsql-php-pdo-pgsql) that generates the authentication token:
```bash
composer require awslabs/aurora-dsql-pdo-pgsql
```
```php
use Aws\AuroraDsql\PdoPgsql\AuroraDsql;
use Aws\AuroraDsql\PdoPgsql\DsqlConfig;
$pdo = AuroraDsql::connect(new DsqlConfig(host: '.dsql..on.aws'));
```
The Lambda function needs the same `dsql:DbConnectAdmin` permission as in the Laravel example above. The application then has to deal with [the differences with PostgreSQL](#differences-with-postgresql) itself, for example by retrying the transactions that conflict.
## Monitoring
The database page in [Bref Cloud](https://bref.sh/cloud) shows the activity of the database: usage (DPUs), transactions, transactions aborted by conflicts, new connections per second, commit latency and storage. It also estimates the cost of the database over the last 30 days, and lists its backups.
Aurora DSQL publishes its metrics in [CloudWatch](https://console.aws.amazon.com/cloudwatch/home), where you can explore them and set up alarms.
---
Source: https://bref.sh/docs/environment/database-planetscale
# Using PlanetScale with Bref on AWS Lambda
[PlanetScale](https://planetscale.com/) is a hosted serverless MySQL database based on the Vitess engine ([learn more](https://planetscale.com/docs/concepts/what-is-planetscale)).
Amongst other features, it offers the following benefits compared to running a database on AWS:
- Simple to set up: no VPC (virtual private network) to set up, no instances to configure.
- Runs [the Vitess clustering system](https://planetscale.com/blog/vitess-for-the-rest-of-us), which offers great scalability and supports a lot more concurrent connections [via built-in connection pooling](https://planetscale.com/blog/one-million-connections).
- Since it does not require a VPC, we do not need to set up and pay [for a NAT Gateway](https://bref.sh/docs/environment/database#vpc-databases-private-network).
One extra feature worth mentioning is [the branching concept](https://planetscale.com/docs/concepts/branching): it enables testing schema changes before deploying them in production without downtime.
[Plans](https://planetscale.com/pricing) start at $39/month for MySQL databases (PlanetScale also offers PostgreSQL databases, starting at $5/month).
## Getting started
To use PlanetScale with Bref, start [by creating a PlanetScale account](https://planetscale.com/).
Then, create a database in the same region as your Bref application.

> [!TIP]
>
> The database is created with an initial development branch: `main`. PlanetScale [has a branching concept](https://planetscale.com/docs/concepts/branching) that lets you test schema changes in a development branch, then promote it to production, or even create new branches (isolated copies of the production schema) off of production to use for development.
You can now click the **Connect** button and select "Connect with: PHP (PDO)". That will let you retrieve the host, database name, user and password.
Here is a simple example that connects to the database using PDO and performs a few queries:
```php
';
$dbname = '';
$user = '';
$password = '';
$pdo = new PDO("mysql:host=$host;dbname=$dbname", $user, $password, [
PDO::MYSQL_ATTR_SSL_CA => openssl_get_cert_locations()['default_cert_file'],
]);
$pdo->exec('CREATE TABLE IF NOT EXISTS test (id INT NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, PRIMARY KEY (id))');
$pdo->exec('INSERT INTO test (name) VALUES ("test")');
var_dump($pdo->query('SELECT * FROM test')->fetchAll());
```
Note the `PDO::MYSQL_ATTR_SSL_CA` flag: while we connect via a username and password, [the connection happens over SSL](https://planetscale.com/docs/concepts/secure-connections) to secure against man-in-the-middle attacks. To avoid hardcoding the location of the file containing SSL certificates, we retrieve its path via `openssl_get_cert_locations()['default_cert_file']`.
In Bref, the file is located here: `/opt/bref/ssl/cert.pem`.
## Laravel
> [!TIP]
>
> This guide assumes you have already set up a Laravel application by following [the Bref documentation for Laravel](https://bref.sh/docs/laravel/getting-started).
To configure Laravel to use the PlanetScale database, you need to set it up via environment variables.
If you deploy a `.env` file, set up the following variables:
```bash filename=".env"
DB_CONNECTION=mysql
DB_HOST=
DB_PORT=3306
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=
# Connect via SSL (https://planetscale.com/docs/concepts/secure-connections)
MYSQL_ATTR_SSL_CA=/opt/bref/ssl/cert.pem
```
If you don't deploy the `.env` file, you can configure the variables in `serverless.yml`:
```yml filename="serverless.yml"
provider:
# ...
environment:
DB_HOST:
DB_DATABASE:
DB_USERNAME:
DB_PASSWORD: ${ssm:/my-app/database-password}
# Connect via SSL (https://planetscale.com/docs/concepts/secure-connections)
MYSQL_ATTR_SSL_CA: /opt/bref/ssl/cert.pem
```
Note that the `DB_PASSWORD` value is sensitive and can be set up as a secret via SSM. Read about [Secret variables](https://bref.sh/docs/environment/variables#secrets) to learn more.
Don't forget to deploy the changes:
```bash
serverless deploy
```
Now that Laravel is configured, you can run `php artisan migrate` in AWS Lambda to set up our tables:
```bash
serverless bref:cli --args="migrate --force"
```
Note: foreign key constraints are disabled by default on PlanetScale. If your migrations use them, enable them in the database settings before running `php artisan migrate`. See the [MySQL compatibility section](#mysql-compatibility) for more information.
That's it! Our database is ready to use.
## Symfony
> [!TIP]
>
> This guide assumes you have already set up a Symfony application by following [the Bref documentation for Symfony](https://bref.sh/docs/symfony/getting-started).
First, make sure you have installed Doctrine, or [follow these docs to do so](https://symfony.com/doc/current/doctrine.html#installing-doctrine).
To configure Symfony to use the PlanetScale database, you need to set it up via environment variables.
If you deploy a `.env` file, set up the following variables:
```bash filename=".env"
DATABASE_URL="mysql://:@:3306/?serverVersion=8.0"
```
If you don't deploy the `.env` file, you can configure the variables in `serverless.yml`:
```yml filename="serverless.yml"
provider:
# ...
environment:
DATABASE_URL: ${ssm:/my-app/database-url}
```
Note that the `DATABASE_URL` value is sensitive and can be set up as a secret via SSM. Read about [Secret variables](https://bref.sh/docs/environment/variables#secrets) to learn more.
Finally, edit the `config/packages/doctrine.yaml` configuration file to set up [the SSL connections](https://planetscale.com/docs/concepts/secure-connections):
```yml filename="config/packages/doctrine.yaml" {6}
doctrine:
dbal:
url: '%env(resolve:DATABASE_URL)%'
options:
# Connect to the database via SSL
!php/const PDO::MYSQL_ATTR_SSL_CA: /opt/bref/ssl/cert.pem
# ...
```
Let's deploy the changes:
```bash
serverless deploy
```
Now that Symfony is configured, you can run the `bin/console doctrine:migrations:migrate` command in AWS Lambda to set up our tables:
```bash
serverless bref:cli --args="doctrine:migrations:migrate"
```
Note: foreign key constraints are disabled by default on PlanetScale. If your migrations use them, enable them in the database settings before running your migrations. See the [MySQL compatibility section](#mysql-compatibility) for more information.
That's it! Our database is ready to use.
## MySQL compatibility
PlanetScale is based on the Vitess clustering system, which was built for scaling MySQL. Because of that, Vitess doesn't support all MySQL features.
The biggest difference is about **foreign key constraints**. They are [supported since February 2024](https://planetscale.com/blog/foreign-key-constraints-are-now-generally-available) on unsharded databases, but they are **disabled by default**: enable them in the database settings ("Allow foreign key constraints") if your application needs them. Read about [their limitations](https://planetscale.com/docs/vitess/foreign-key-constraints) before doing so.
If you keep them disabled, it is still possible to have references between rows of different tables and perform joins. But constraints are not enforced: foreign keys are not validated at the database level, and you cannot use `ON DELETE ...` statements.
That means that you should take care of validating references between rows and handle cascade deletions. If you use an ORM, you should be in a good place:
- **Laravel** DB migrations and Eloquent work fine. You can use [the `foreignId()` method](https://laravel.com/docs/migrations#foreign-key-constraints) to create relationships between tables, but you cannot enforce referential integrity with the `constrained()` method (and related methods like `onDelete('cascade')`).
- [**Doctrine**](https://www.doctrine-project.org/) works fine, but you should not use [`onDelete="CASCADE"` which relies on foreign key constraints](https://www.doctrine-project.org/projects/doctrine-orm/en/2.14/reference/working-with-objects.html#removing-entities) (`cascade=REMOVE` or `cascade=ALL` is fine as Doctrine performs the cascade in memory via PHP).
You can read the [complete **MySQL compatibility table** on the PlanetScale website](https://planetscale.com/docs/reference/mysql-compatibility).
## Database import
PlanetScale provides an automated import tool to import an existing database without downtime. Check out [the documentation](https://planetscale.com/docs/imports/database-imports) to get started.
For simple scenarios, you can also use the [`mysqldump` tool](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html#mysqldump-syntax) to export your existing database and import it later in PlanetScale. Note that there are [specific options you need to use for Vitess](https://vitess.io/docs/15.0/user-guides/configuration-basic/exporting-data/#mysqldump). If foreign key constraints are disabled on your PlanetScale database, you also need to export the schema and the data separately so that you can remove the constraints from the schema.
Let's first export the schema and the data:
```bash
mysqldump -u -p -h --set-gtid-purged=OFF --no-tablespaces --no-data > schema.sql
mysqldump -u -p -h --set-gtid-purged=OFF --no-tablespaces --no-create-info > data.sql
```
Next, if foreign key constraints are disabled, edit `schema.sql` to remove them ([learn more](https://planetscale.com/docs/vitess/operating-without-foreign-key-constraints)), for example:
```diff filename="schema.sql"
CREATE TABLE products (
id INT NOT NULL,
category_id INT,
PRIMARY KEY (id),
- KEY category_id_idx (category_id),
- CONSTRAINT `category_fk` FOREIGN KEY (category_id) REFERENCES category(id)
+ KEY category_id_idx (category_id)
);
```
(watch out for the trailing comma, else you might get errors like "You have an error in your SQL syntax")
Finally, you can import the `schema.sql` and `data.sql` into PlanetScale, **using the PlanetScale settings this time** (user, password, host):
```bash
mysql -u -p -h < schema.sql
mysql -u -p -h < data.sql
```
## Schema changes workflow
PlanetScale has a concept of [database branches](https://planetscale.com/docs/concepts/branching):
- [**Development** branches](https://planetscale.com/docs/concepts/branching#development-and-production-branches) are isolated copies of your production database and are used to test schema changes in development.
- [**Production** branches](https://planetscale.com/docs/concepts/branching#development-and-production-branches) are high availability branches intended for production traffic. They are protected from direct DDL, so you cannot perform direct schema changes on production branches.
You can set up production branches in two ways:
- either **allow** running DB migrations directly on your production database,
- or **forbid** direct schema changes in production by enabling [_Safe Migrations_](https://planetscale.com/docs/concepts/safe-migrations).
### Without "Safe Migrations"
If the production branch has the "Safe Migrations" feature **disabled**, you can run DB migrations on the production database as part of your deployment.
This strategy implies either:
- accepting downtime on deployment, for example by using [Laravel's maintenance mode](https://bref.sh/docs/laravel/maintenance-mode) (put the app offline, deploy, run migrations, then put the app back online)
- or always writing backward-compatible DB migrations
This option works well for applications with low traffic or in early development. For high-traffic applications, using "Safe Migrations" is recommended instead.
### With "Safe Migrations"
If the production branch has the "Safe Migrations" feature **enabled**, you need to use development branches and deploy requests. Below is an introduction to these features.
You start with a development branch called `main`, which lets you set up your schema. Once set up, you can [promote](https://planetscale.com/docs/concepts/branching#promote-a-branch-to-production) that branch (or any other branch) to a production branch with "Safe Migrations" enabled.
Later, you can apply schema changes (aka DB migrations) to the production database **without downtime**:
1. In PlanetScale, create a new development branch off of the production branch. This is an isolated copy of the production schema that you can freely play around with.
2. Set up a dev environment of your application.
3. Run the DB migrations in the dev branch.
4. Test the changes in the dev environment.
5. In PlanetScale, deploy the DB changes to the production branch via a "deploy request".
Let's dive into these steps in the next section.
#### Deploying DB migrations in detail
PlanetScale works with **branches**, which matches the concept of **stages** in Serverless Framework.
To deploy DB migrations in production, you can work with two environments:
- A **production** environment: our application deployed in the `prod` stage and configured to use the `production` PlanetScale branch.
- A **dev** environment: our application deployed in the `dev` stage and configured to use the `development` PlanetScale branch.
You can [deploy our applications to different stages](https://bref.sh/docs/deploy#environments) via the `--stage` option. Each stage is completely isolated from the others.
```bash
# Deploy the "dev" environment:
serverless deploy
# Deploy the "prod" environment:
serverless deploy --stage=prod
```
You want each "stage" of our application to connect to a different PlanetScale branch. You can achieve that in `serverless.yml` via [stage parameters](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/parameters.md#stage-parameters):
```yml filename="serverless.yml"
provider:
# ...
environment:
# ...
DB_HOST:
DB_DATABASE:
DB_USERNAME: ${param:db-user}
DB_PASSWORD: ${param:db-password}
params:
# These values apply for all stages by default
default:
# Connect to the "development" database in PlanetScale
db-user:
db-password: ${ssm:/my-app/dev/db-password}
# These values apply for the `prod` stage only
prod:
# Connect to the "production" database in PlanetScale
db-user:
db-password: ${ssm:/my-app/prod/db-password}
```
In the example above, the `DB_USERNAME` and `DB_PASSWORD` environment variables will have different values based on the stage.
The next step is to [create the `development` branch off of the production branch](https://planetscale.com/docs/concepts/branching#create-a-development-branch). This branch will be an isolated copy of the production schema.
Now that the environments are set up, you can apply the following workflow for DB migrations:
1. Deploy your code changes and migrations in the development stage.
1. Apply DB migrations in the **development** environment (drop a column, add a table, etc.):
- If you use Laravel, run DB migrations [via the `artisan` function](https://bref.sh/docs/laravel/getting-started#laravel-artisan): `serverless bref:cli --stage=dev --args="migrate"`
- If you use Symfony, run DB migrations [via the `console` function](https://bref.sh/docs/symfony/getting-started#symfony-console): `serverless bref:cli --stage=dev --args="doctrine:migrations:migrate"`
- If you don't use any framework, run DB queries [via the `pscale` CLI](https://planetscale.com/docs/reference/planetscale-cli).
1. Test changes in the development environment to make sure everything works correctly.
1. Create a deploy request.
PlanetScale will create a schema diff that you can review before applying. It will also validate the diff and detect schema issues like missing unique keys, etc.
Once reviewed and approved, you can add it to the deploy queue and PlanetScale will begin the deployment.
The schema changes are deployed without downtime: no table gets locked, and production is not slowed down during the migration. This is what is called "[non-blocking schema changes](https://planetscale.com/docs/concepts/nonblocking-schema-changes)".
The migrations are now applied to production.
### When to apply migrations?
Depending on the schema change, you might want to apply DB migrations _before_ or _after_ a code deployment:
- **Add a column/table:** apply the migration _before_ deploying the code.
- **Remove a column/table:** apply the migration _after_ deploying the code.
- **Rename a column/table:** this scenario is more complex and needs to be addressed in two steps:
- Apply migrations that **add** the column/table (it will be duplicated).
- Deploy code changes to write new data to the new column/table, and read from both.
- Run a script that copies the old data to the new column/table.
- Deploy code changes to only read and write to the new column/table.
- Apply migrations that **remove** the old column/table.
If you use Laravel, you can read a complete blog post about this topic: [Zero downtime Laravel migrations](https://planetscale.com/blog/zero-downtime-laravel-migrations#when-to-run-migrations).
---
Source: https://bref.sh/docs/environment/emails
# Sending emails
Applications running on AWS Lambda can send emails with any email provider: Mailgun, Postmark, Resend, etc. are configured the same way as on a server. Bref does not require nor recommend a specific provider.
This page documents how to use [Amazon SES](https://aws.amazon.com/ses/) (Simple Email Service), the email service of AWS. Unlike other AWS services in this documentation, like SQS or S3, using SES is entirely optional.
Compared to other email providers, SES has some advantages on AWS Lambda:
- **No API key**: the application sends emails with its IAM role, there is no secret to store.
- **Same AWS account** as the application, billed per email ([$0.10 per 1,000 emails](https://aws.amazon.com/ses/pricing/)).
But SES does not list sent emails, bounces or complaints in a dashboard out of the box.
## Verifying a domain
SES only sends emails from verified domains or addresses. Create the domain identity **in the same region as your application**.
Send emails from a subdomain, for example `mail.example.com`: its sending reputation is separate from the main domain, and the main domain keeps its MX records to receive emails.
Open the [SES identities page](https://console.aws.amazon.com/ses/home#/identities) in the region of your application, and click "Create identity":
- select "Domain" and enter `mail.example.com`,
- check "Use a custom MAIL FROM domain" and enter `bounce` (the MAIL FROM domain is then `bounce.mail.example.com`),
- keep "Easy DKIM" selected.
The page of the identity then lists the DNS records to create.
Create the domain identity:
```bash
aws sesv2 create-email-identity --region eu-west-3 --email-identity mail.example.com
```
SES signs emails with DKIM keys it manages ("Easy DKIM"). The command returns three DKIM tokens: each one is a CNAME record to create (see below).
Then set the MAIL FROM domain of the identity:
```bash
aws sesv2 put-email-identity-mail-from-attributes --region eu-west-3 \
--email-identity mail.example.com \
--mail-from-domain bounce.mail.example.com \
--behavior-on-mx-failure USE_DEFAULT_VALUE
```
The MAIL FROM domain is the address bounces are sent to (the `Return-Path` header). By default, SES uses a subdomain of `amazonses.com`, which means SPF does not align with your domain and does not count for DMARC. With `USE_DEFAULT_VALUE`, SES falls back to `amazonses.com` if the MX record of the MAIL FROM domain is missing, instead of rejecting emails.
Create the following DNS records. If you use Cloudflare, disable the proxy on the CNAME records ("DNS only").
| Type | Name | Value |
|---|---|---|
| CNAME (×3) | `._domainkey.mail.example.com` | `.dkim.amazonses.com` |
| MX (priority 10) | `bounce.mail.example.com` | `feedback-smtp..amazonses.com` |
| TXT | `bounce.mail.example.com` | `"v=spf1 include:amazonses.com ~all"` |
| TXT | `_dmarc.example.com` | `"v=DMARC1; p=none"` |
The DMARC record applies to the main domain and all its subdomains. Skip it if your domain already has one. `p=none` only reports failures: once all the emails sent from your domain pass DMARC, you can switch to `p=quarantine` or `p=reject`.
SES verifies the domain a few minutes after the DNS records are created. Both statuses returned by this command should be `SUCCESS`:
```bash
aws sesv2 get-email-identity --region eu-west-3 --email-identity mail.example.com \
--query '{dkim: DkimAttributes.Status, mailFrom: MailFromAttributes.MailFromDomainStatus}'
```
### Receiving replies
The `mail.example.com` subdomain has no MX record, so it cannot receive emails. Set a `Reply-To` address on a domain that receives emails, for example `contact@example.com`. See [the Laravel example below](#laravel).
## Allowing the application to send emails
Allow the application to send emails from the domain identity in `serverless.yml`:
```yml filename="serverless.yml"
provider:
iam:
role:
statements:
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
Resource: arn:aws:ses:${aws:region}:${aws:accountId}:identity/mail.example.com
# Optional: restrict the sender address
Condition:
StringEquals:
ses:FromAddress: contact@mail.example.com
```
`ses:SendRawEmail` is required even with the SES v2 API: Laravel and Symfony send the full MIME message (raw content), which AWS checks as `ses:SendRawEmail`. Without it, sending fails with:
```
User: arn:aws:sts::…:assumed-role/… is not authorized to perform: ses:SendRawEmail on resource: arn:aws:ses:…:identity/mail.example.com
```
> [!WARNING]
>
> While the AWS account is in the [SES sandbox](#leaving-the-ses-sandbox), AWS also checks the identity of the **recipient**: the policy above denies emails sent to verified test addresses. Add these addresses to `Resource` during tests (for example `arn:aws:ses:${aws:region}:${aws:accountId}:identity/test@example.com`), and remove them once the account has production access.
## Laravel
Laravel sends emails with SES through the AWS SDK:
```bash
composer require aws/aws-sdk-php
```
In `config/mail.php`, switch the transport of the `ses` mailer to `ses-v2`, to use the SES v2 API (the API AWS keeps evolving):
```php filename="config/mail.php"
'mailers' => [
// ...
'ses' => [
'transport' => 'ses-v2',
],
],
```
Then select the `ses` mailer, and set the sender address, in `serverless.yml`:
```yml filename="serverless.yml"
provider:
environment:
MAIL_MAILER: ses
MAIL_FROM_ADDRESS: contact@mail.example.com
MAIL_FROM_NAME: My application
```
There is no need to change the `ses` configuration in `config/services.php`: on AWS Lambda, Bref's Laravel integration configures SES with the credentials of the Lambda function. The region is the region of the application.
To receive replies (see [above](#receiving-replies)), set a global `Reply-To` address in `config/mail.php`:
```php filename="config/mail.php"
'reply_to' => [
'address' => env('MAIL_REPLY_TO_ADDRESS'),
'name' => env('MAIL_FROM_NAME'),
],
```
> [!WARNING]
>
> If your `composer.json` [removes unused AWS services](https://bref.sh/docs/deploy#reducing-package-size) from the AWS SDK, add `SesV2` to the services it keeps, then run `composer install`. Otherwise, sending emails fails in production with `Class "Aws\SesV2\SesV2Client" not found`.
>
> ```json filename="composer.json"
> "extra": {
> "aws/aws-sdk-php": [
> "Sqs",
> "SesV2"
> ]
> }
> ```
>
> Tests and local development don't catch this error if they install dependencies with `composer install --no-scripts`: the unused services are only removed when the scripts run.
## Symfony
> [!WARNING]
>
> This Symfony configuration has not been tested yet. If it works for you, please [send a pull request](https://github.com/brefphp/bref/edit/master/docs/environment/emails.mdx) that removes this warning to confirm it.
Install the Amazon SES transport for Symfony Mailer:
```bash
composer require symfony/amazon-mailer
```
Configure it in `serverless.yml`, without access key in the DSN: the transport then uses the credentials of the Lambda function.
```yml filename="serverless.yml"
provider:
environment:
MAILER_DSN: ses+api://default?region=${aws:region}
```
Read more in the [Symfony Mailer documentation](https://symfony.com/doc/current/mailer.html#using-a-3rd-party-transport).
## Other PHP applications
Use the `SesV2Client` of the AWS SDK. On AWS Lambda, it uses the credentials and the region of the Lambda function:
```php
$ses = new \Aws\SesV2\SesV2Client([]);
$ses->sendEmail([
'FromEmailAddress' => 'contact@mail.example.com',
'Destination' => [
'ToAddresses' => ['john@example.com'],
],
'Content' => [
'Simple' => [
'Subject' => ['Data' => 'Hello'],
'Body' => [
'Text' => ['Data' => 'Hello world!'],
],
],
],
]);
```
## Leaving the SES sandbox
In each region, a new AWS account starts in the [SES sandbox](https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html): it can send up to 200 emails per 24 hours, and only to verified addresses.
To send emails to anyone, open the [SES account dashboard](https://console.aws.amazon.com/ses/home#/account) in the region of your application, and click "Request production access". The region must have a verified identity.
AWS usually replies within a day, often with questions about how you send emails. Answer all of them in one message to avoid back-and-forth:
- the URL of the website: deploy the application on its real domain first, as reviewers look at it,
- the type of emails (for example login links and notifications) and the expected volume,
- how recipients sign up to receive them,
- how bounces and complaints are handled: the account-level [suppression list](https://docs.aws.amazon.com/ses/latest/dg/sending-email-suppression-list.html) (enabled by default) stops sending to addresses that bounced or complained,
- an example email.
---
Source: https://bref.sh/docs/environment/aws-credentials
# AWS credentials on AWS Lambda
When your PHP application runs on AWS Lambda, it automatically has access to AWS credentials. This means you don't need to manage AWS access keys or credentials in your code - Lambda handles this for you.
> [!WARNING]
>
> Don't deploy AWS access keys in your Lambda functions or environment variables. Lambda provides credentials automatically.
>
> This is a common mistake **when migrating an existing application to AWS Lambda**.
## How it works
Lambda functions **automatically get AWS access keys** in their environment variables. These credentials are temporary and managed by AWS, so you don't have to worry about rotating them or keeping them secure.
```php
echo $_SERVER['AWS_ACCESS_KEY_ID']; // AKIAIOSFODNN7EXAMPLE
echo $_SERVER['AWS_SECRET_ACCESS_KEY']; // wJalrXUtnFEM
echo $_SERVER['AWS_SESSION_TOKEN']; // AQoEXAMPLEH4aoAH0gNCAPy...
```
The PHP AWS SDK automatically detects and uses them. Here's an example with S3:
```php
$s3 = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $_SERVER['AWS_REGION'],
// No credentials needed, the SDK uses the environment variables automatically
]);
// Use S3 normally
$result = $s3->putObject([
'Bucket' => 'my-bucket',
'Key' => 'file.txt',
'Body' => 'Hello from Lambda!'
]);
// Note that this also works with https://async-aws.com
```
Note that **Laravel and Symfony automatically pick up these permissions** too.
These credentials have access controlled by an IAM role defined in `serverless.yml`.
> [!NOTE]
>
> By default, Lambda functions **don't have any access** (principle of least privilege). To access other AWS services (like S3 or SQS), you need to add permissions to that IAM role in `serverless.yml` (read below).
## Adding permissions
To grant your Lambda function access to AWS services, add IAM statements to your `serverless.yml`:
```yaml
service: my-app
provider:
name: aws
iam:
role:
statements:
# IAM statements here...
functions:
# ...
```
### Example: S3
To read and write files to an S3 bucket:
```yaml
provider:
name: aws
iam:
role:
statements:
# Allow Lambda to read and write to S3
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource: arn:aws:s3:::my-bucket/*
# Allow listing bucket contents
- Effect: Allow
Action: s3:ListBucket
Resource: arn:aws:s3:::my-bucket
```
> [!TIP]
>
> If you use the [Lift `storage` construct](https://bref.sh/docs/environment/storage#s3-storage) to create S3 buckets, it [automatically adds the necessary permissions](https://github.com/getlift/lift/blob/master/docs/storage.md#permissions) to your functions. No need to set up permissions manually!
### Example: SQS
To send and receive messages from SQS queues:
```yaml
provider:
name: aws
iam:
role:
statements:
# Allow Lambda to access an SQS queue
- Effect: Allow
Action:
- sqs:SendMessage
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
Resource: arn:aws:sqs:${aws:region}:${aws:accountId}:my-queue
```
> [!TIP]
>
> If you use the [Lift `queue` construct](https://bref.sh/docs/use-cases/sqs#creating-sqs-queues) to create SQS queues, it [automatically adds the necessary permissions](https://github.com/getlift/lift/blob/master/docs/queue.md#permissions) to your functions. No need to set up permissions manually!
## Common services and permissions
Here are the IAM actions you'll typically need for common AWS services:
### DynamoDB
```yaml
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
- dynamodb:Query
- dynamodb:Scan
Resource: arn:aws:dynamodb:${aws:region}:${aws:accountId}:table/my-table
```
### Secrets Manager
```yaml
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: arn:aws:secretsmanager:${aws:region}:${aws:accountId}:secret:my-secret-*
```
### SNS (notifications)
```yaml
- Effect: Allow
Action: sns:Publish
Resource: arn:aws:sns:${aws:region}:${aws:accountId}:my-topic
```
### EventBridge
```yaml
- Effect: Allow
Action: events:PutEvents
Resource: arn:aws:events:${aws:region}:${aws:accountId}:event-bus/my-event-bus
```
### SSM Parameter Store
```yaml
- Effect: Allow
Action:
- ssm:GetParameter
- ssm:GetParameters
Resource: arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/my-app/*
```
## Troubleshooting
### Access Denied errors
If you get "Access Denied" errors when trying to use AWS services:
1. Check that you've added the correct IAM permissions in `serverless.yml`
2. Verify the resource ARN is correct (bucket name, queue name, etc.)
3. Make sure you've redeployed after adding permissions
4. [Check the logs](https://bref.sh/docs/environment/logs) for the exact error message
### Testing locally
When testing locally remember that you will need to provide AWS credentials since you're not running on Lambda. You can set them up via long-lived AWS access keys or IAM roles with SSO.
## Permissions per function
If you want to define permissions **per function**, instead of globally (i.e. in the `provider` section), you can install the plugin [`serverless-iam-roles-per-function`](https://github.com/functionalone/serverless-iam-roles-per-function) and then use the `iamRoleStatements` at the function definition block.
## Learn more
- [`serverless.yml` IAM guide](https://github.com/oss-serverless/osls/blob/4.x/docs/guides/iam.md)
- [Documentation of the AWS SDK for PHP](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/)
---
Source: https://bref.sh/docs/environment/cold-starts
# Cold starts
If your application cannot tolerate any response above 300ms (e.g. real-time trading, multiplayer gaming), Lambda is not the right fit.
For everything else, cold starts are often the most overestimated concern when moving to serverless. On applications with regular traffic, cold starts represent about **0.2% of requests**. For the vast majority of applications, their impact is negligible.
## What is a cold start?
AWS Lambda runs code on-demand. When a new Lambda instance boots to handle a request, the initialization time is called a *cold start*.
Once initialized, the instance stays warm and handles subsequent requests with no cold start. Lambda keeps instances alive for several minutes after the last request. As long as your application receives regular traffic, most requests are handled by warm instances.
You can learn more about how Bref and Lambda work in [How Bref works](https://bref.sh/docs/how-it-works), and about how Lambda scales in [Serverless Visually Explained](https://serverless-visually-explained.com/).
## Cold start duration
Bref's PHP runtimes add a cold start of about **250ms** on average. The rest depends on the size of your application.
To put it differently: on average (application with traffic), out of 1000 requests, 998 are as fast as on a traditional server. 2 requests have an extra cold start latency.
This is on par with [cold starts in other languages](https://mikhail.io/serverless/coldstarts/aws/) like JavaScript, Python or Go, and Bref's runtimes are optimized as much as possible.
## Warming for low-traffic applications
If your application has very low traffic (e.g. a new project or an internal tool), your Lambda functions might scale down to 0 instances. Cold starts will then happen more often.
You can pre-warm your HTTP function by adding a scheduled event in `serverless.yml`:
```yml filename="serverless.yml"
functions:
web:
handler: public/index.php
runtime: php-84-fpm
timeout: 15
events:
- httpApi: '*'
- schedule:
rate: rate(5 minutes)
input:
warmer: true
```
Bref recognizes the `warmer` event and responds with a `Status: 100` in a few milliseconds without executing your application code. This keeps the Lambda instance warm.
You can also use external services like [Pingdom](https://www.pingdom.com/) to ping your application regularly.
## Provisioned concurrency
AWS offers [provisioned concurrency](https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html) to keep a set number of Lambda instances initialized at all times. This completely eliminates cold starts for those instances.
This is useful for applications that need consistently low latency but is more expensive since you pay for the instances even when they are idle. For most applications, the warming approach above is simpler and sufficient.
## Reducing cold start duration
The codebase size can increase the cold start duration. When deploying, exclude unnecessary files in `serverless.yml`:
```yml filename="serverless.yml"
package:
patterns:
- '!assets/**'
- '!node_modules/**'
- '!tests/**'
- ...
```
Read more about this [in the serverless.yml documentation](https://bref.sh/docs/environment/serverless-yml#exclusions).
---
Source: https://bref.sh/docs/environment/performances
# Performance
This article sums up what to expect in terms of performance and how to optimize serverless PHP applications. The benchmarks included in this page can be reproduced via [the code on GitHub](https://github.com/brefphp/benchmarks).
## CPU power and memory size
The main factor affecting performance is memory size. Indeed, [the amount of memory is proportional to the CPU power available](https://docs.aws.amazon.com/lambda/latest/dg/resource-model.html).
In other words, **more memory means a more powerful CPU**. A 1024M lambda has a CPU two times more powerful than a 512M lambda.
From 128M to 1,769M, applications run with up to one vCPU (1,769M gives 1 full vCPU). Memory can go up to 10,240M, which provides up to 6 vCPUs. Since PHP is single-threaded and one lambda handles only 1 request at a time, using more than 1 vCPU usually does not provide any benefit.
**It is recommended to use 1024M** for PHP applications, or at least to start with that. This is what Serverless deploys by default, so there is nothing to do.
To customize the amount of memory, set the `memorySize` option in `serverless.yml`:
```yml filename="serverless.yml" {5}
functions:
foo:
handler: index.php
# ...
memorySize: 512 # set to 512M instead of 1024M (the default)
```
In the benchmark below, we run [PHP's official `bench.php` script](https://github.com/php/php-src/blob/master/Zend/bench.php). This script is CPU-intensive.
| | 128M | 512M | 1024M | 2048M |
|------------------|------:|-----:|------:|------:|
| Execution time | 5.7s | 1.4s | 0.65s | 0.33s |
For comparison, `bench.php` runs in 1.3s on a 512M [Digital Ocean](https://www.digitalocean.com/) server, in 0.8s on a 2.8Ghz i7 and in 0.6s on a 3.2Ghz i5. It is safe to say that a 1024M lambda provides a powerful CPU.
### Costs
AWS Lambda bills the number of events + the execution time. The more memory configured for a lambda, [the more expensive the execution time is](https://aws.amazon.com/lambda/pricing/).
It might be tempting to lower the memory to save money. However, a function might run slower on a smaller lambda, canceling the cost savings. For example, both of these scenarios cost the same thing:
- a function running in 400ms on a 512M lambda
- the same function running in 200ms (because of the faster CPU) on a 1024M lambda
In general, **use smaller and slower lambdas only when speed is not important at all.**
## PHP runtime overhead
### Bref for web apps
The [FPM runtime for web apps](https://bref.sh/docs/runtimes/fpm-runtime) **does not add overhead to response times**.
Here are execution times for an empty PHP application:
| | 128M | 512M | 1024M | 2048M |
|------------------|------:|-----:|------:|------:|
| Execution time | 10ms | 1ms | 1ms | 1ms |
Unless we use a particularly slow lambda (see the previous section, 128M is not recommended), 1ms is the same execution time when PHP runs with Apache or Nginx on a classic server.
We can see the same result with a "Hello world" written in Symfony (4ms being the minimum execution time of the framework):
| | 128M | 512M | 1024M | 2048M |
|------------------|------:|-----:|------:|------:|
| Execution time | 58ms | 4ms | 4ms | 4ms |
### Bref for event-driven functions
The [runtime for event-driven functions](https://bref.sh/docs/runtimes/function) adds a small overhead:
| | 128M | 512M | 1024M | 2048M |
|------------------|------:|-----:|------:|------:|
| Execution time | 175ms | 35ms | 16ms | 13ms |
Since this runtime is often used in asynchronous scenarios (for example, processing queue messages), it is often negligible.
This overhead is caused by the PHP executable starting for every new invocation.
We can skip that overhead by keeping the PHP process alive:
```yml filename="serverless.yml" {5}
functions:
hello:
# ...
environment:
BREF_LOOP_MAX: 100
```
In the example above, the PHP process will restart only every 100 invocations, removing the overhead the rest of the time.
In that case, be careful with clearing in-memory data between every event.
> [!TIP]
>
> Note: the PHP process will be restarted in case of a failed invocation (PHP exception thrown in the handler).
## Cold starts
On applications with regular traffic, cold starts only represent about **0.2% of requests**. Bref's PHP runtimes add a cold start of about **250ms** on average, which is on-par with other languages.
Read more in the [Cold starts documentation](https://bref.sh/docs/environment/cold-starts).
---
Source: https://bref.sh/docs/community
# Community
To report bugs you can head over to the [GitHub Bref repository](https://github.com/brefphp/bref).
For community support and general discussions, [open a GitHub discussion](https://github.com/brefphp/bref/discussions).
You can also join the [Slack community](https://bref.sh/slack) to discuss Bref and exchange with the community (please open an issue if the link doesn't work).
On X, follow [@brefphp](https://x.com/brefphp) to get news about Bref.
## Contributors
The full list of contributors to Bref is [available here](https://github.com/brefphp/bref/graphs/contributors).
## Newsletters
Here are a few newsletters related to serverless and PHP. Those are not necessarily related to Bref.
You can subscribe to these newsletters to keep up to date with what's happening in the serverless world.
- [Serverless PHP](https://serverless-php.news/)
A newsletter about serverless news related to PHP.
- [Off by None](https://www.jeremydaly.com/newsletter/)
A very packed weekly newsletter about serverless news in general.
---
Source: https://bref.sh/docs/case-studies
# Case studies
This page collects case studies of serverless PHP applications built with or migrated to Bref.
They help you learn about costs, performance and migration efforts from real use cases.
## Applications
- [Craft Cloud](https://bref.sh/docs/case-studies/case-studies/craft-cloud)
How Craft Cloud runs Craft CMS projects at scale on AWS Lambda with Bref.
- [Treezor](https://bref.sh/docs/case-studies/case-studies/treezor)
How Treezor, a banking platform, went from legacy code on servers to a serverless architecture with Bref.
- [Spreaker](https://careers.spreaker.com/engineering/rebuilding-spreaker-web-listening-experience-with-php-and-serverless/)
How Spreaker, a podcast hosting platform, rewrote a decade-old monolith with Bref, Laravel Octane and Livewire.
- [externals.io](https://mnapoli.fr/serverless-case-study-externals/)
A case study of the migration of [externals.io](https://externals.io/) to AWS Lambda using Bref. This includes performance and costs details.
- [returntrue.win](https://mnapoli.fr/serverless-case-study-returntrue/)
A case study of the development of the [returntrue.win](https://returntrue.win/) website using AWS Lambda, including a cost analysis.
## Workers
- 🇫🇷 [Enoptea](https://www.enoptea.fr/serverless-et-php/)
Enoptea is a French startup that migrated their infrastructure of PHP workers from EC2 to Lambda. They halved their AWS costs and increased their performance while spending less time managing their servers.
- [PrettyCI.com](https://mnapoli.fr/serverless-case-study-prettyci/)
PrettyCI was a SaaS providing continuous integration for PHP coding standards. Internally, it runs PHP-CS-Fixer or CodeSniffer on AWS Lambda using Bref. This article is a good introduction on how AWS Lambda can be a good solution to run workers and background jobs.
- [MyBuilder](https://mybuilder.com)
MyBuilder is an online marketplace matching tradespeople with home owners. They used Lambda with Bref to create a highly scalable on-demand microservice to generate PDF reports. The solution involved [creating their own layer](https://web.archive.org/web/20210505204331/https://tech.mybuilder.com/compiling-wkhtmltopdf-aws-lambda-with-bref-easier-than-you-think/) to include a self-compiled binary file to use alongside Bref's base PHP layer.
- [PDF reporting generation](https://devops-life.com/blog/2020/03/06/how-serverless-saved-us-for-$2-with-bref-sh/)
A case study of going serverless for PDF generation. They generated 2,000 PDFs in less than 2 min for $2 using Symfony and Bref.
## Others
- [Serverless](https://www.serverless.com/category/user-stories)
It isn't exactly about PHP over Serverless, but it gives us a general overview of how hundreds of teams are handling serverless across a diversity of projects. There are a lot of case studies from [US Department of Defense streamlining open source contributions](https://www.serverless.com/blog/dept-of-defense-doc-bot/) to a project which has [reduced back-end costs by 95%](https://www.serverless.com/blog/abstract-partner-program-announcement/) and much more.
---
Source: https://bref.sh/docs/case-studies/craft-cloud
# How Craft CMS built Craft Cloud
This case study dives into how the Craft CMS team created the Craft Cloud hosting platform using serverless technologies and Bref.
[Craft CMS](https://craftcms.com/) is an open-source PHP CMS that powers more than 150,000 websites across the world. Last week, the Craft team launched [Craft Cloud](https://craftcms.com/cloud), a scalable hosting platform for Craft CMS users.
To make Craft Cloud secure and scalable while keeping infrastructure simple to maintain, the Craft team decided to **run Craft CMS serverless**.
To do so, Craft Cloud is built on AWS and Cloudflare and **uses Bref to run Craft CMS (and PHP) at scale on AWS Lambda**. In this case study, let’s explore in more detail how the Craft team built their cloud.
## Architecture overview

Craft Cloud is composed of several applications:
- The **Craft Cloud Console** is the UI that users can use to add, configure, and monitor their projects. The Console also allows users to create organizations, purchase plugins, and Craft licenses.
- The Console talks to the **Craft Cloud API**. The API is responsible for creating and updating the infrastructure for each customer’s project and environments.
- The **Craft Cloud environments** are where the customers' Craft CMS projects run. Each project runs in a separate environment based on a Git branch, ensuring projects and customers are isolated from each other.
## Craft Cloud Console

The Craft Cloud Console is the visible part: the web UI where users can sign up, create projects, and monitor them.
This is a Craft CMS application currently running on AWS ElasticBeanstalk (with plans to move onto Craft Cloud itself later). This UI talks to the Craft Cloud API to perform actions on projects.
## Craft Cloud API
This API is responsible for orchestrating all the customers' Craft Cloud projects: creating projects, databases, queues, as well as deploying the code, setting environment variables, etc.

It is implemented as a Laravel application and runs on AWS Lambda using Bref and [its Laravel bridge](https://bref.sh/docs/laravel/getting-started). Some of its operations can take time (for example when creating new AWS resources), which is why it uses [Laravel Queues set up with SQS](https://bref.sh/docs/laravel/queues). It also uses PostgreSQL and DynamoDB as databases, and S3 to store artifacts.
Each Craft Cloud project is deployed to AWS Lambda via a container image. The API builds these custom container images, one image for each project. That allows each project to run with a specific PHP version, environment variables, PHP extensions, etc.
The Craft Cloud API builds the Lambda container images using [Dagger](https://dagger.io) (Dagger is like writing Dockerfiles using Go), and a custom Go application that orchestrates and inspects the builds. These builds run on ECS (Fargate), as AWS Lambda cannot build Container images directly.
## Craft Cloud environments
Craft Cloud environments are the key part of the infrastructure: they run Craft CMS customer projects. These run serverless with Bref on AWS Lambda, using the container images built by the Craft Cloud API.
In each environment, Craft CMS customer projects have automatic access to a MySQL or PostgreSQL database, S3 for storing files, a job queue preconfigured with SQS, commands, and logs storage in CloudWatch. Customers can also choose the AWS regions in which their application will run.

### AWS Lambda events
Each customer project can run in multiple environments (dev, staging, production, etc.), each running in a single AWS Lambda function.
That means that **one AWS Lambda function handles multiple events**:
- HTTP invocations to serve the web app
- SQS invocations to process [queue jobs](https://craftcms.com/docs/4.x/extend/queue-jobs.html)
- Command invocations to manually trigger [console commands](https://craftcms.com/docs/4.x/console-commands.html)

While unusual on AWS Lambda, the Craft team made this design choice to keep the architecture straightforward (1 function per environment), as well as simplify management. This choice has been working out successfully so far.
To achieve this, the Bref runtimes [had to be customized](https://github.com/craftcms/cloud-extension-yii2/blob/1.x/src/runtime/event/EventHandler.php) so that a central “event router” dispatches AWS Lambda invocations to the correct Craft CMS handler (HTTP, queue, command, etc.).
### NATS.io
When building a multi-regional application, some tasks need to be region-specific for data privacy and performance. Examples of these tasks include builds, database backups, and running Craft Commands. To support region-specific tasks, Craft Cloud uses NATS.io and Synadia Cloud to stream events to the region where the work needs to occur.
### Domain names & CDN
Every Craft Cloud project gets a preview domain name in the form: `xxx.preview.craft.cloud`. Customers are then free to set up their own custom domains.
To serve these dynamic preview domains, Craft Cloud uses the Cloudflare CDN, with its [SSL for SaaS](https://www.cloudflare.com/application-services/products/ssl-for-saas-providers/) feature and its [Cloudflare Workers](https://workers.cloudflare.com/).

Cloudflare Workers are serverless functions written in JavaScript and running in edge locations (unlike AWS Lambda’s default behavior which runs in data centers). Workers also come with [Workers KV](https://developers.cloudflare.com/kv/), a simple and fast key-value storage. Workers are in essence limited: they can only run a subset of JavaScript, do not get access to a filesystem, etc. But these limits allow running Workers in edge locations, close to users, without cold starts, and with very low latency. This makes them very useful for doing dynamic routing: exactly what Craft Cloud does.
Whenever a new project environment is created, the Craft Cloud API creates a new Worker KV storage entry. Then, on every request to `xxx.preview.craft.cloud`, a Worker compares the domain name to the KV database, retrieves the entry for the project/environment, and invokes the AWS Lambda function that runs the Craft CMS project.
The Craft team decided to go with Cloudflare instead of a solution like AWS API Gateway or CloudFront because it did not impose limits on the number of custom domains that could be set up. Another benefit of Cloudflare is that it allows caching entire HTTP responses to help serve websites faster to users. This is a key feature of Craft Cloud: static caching is enabled by default to improve latency on all hosted websites.
### Running Craft CMS and Yii on AWS Lambda
Craft CMS is built on top of the [Yii PHP framework](https://www.yiiframework.com/). While Bref provides native integrations for [Laravel](https://bref.sh/docs/laravel/getting-started) and [Symfony](https://bref.sh/docs/symfony/getting-started), it does not for Yii. That means Yii had to be customized to run on AWS Lambda.
The Craft team published this as an open-source package on GitHub: [github.com/craftcms/cloud-extension-yii2](https://github.com/craftcms/cloud-extension-yii2).
This package pre-configures Craft CMS and Yii to:
- Publish assets to S3 so that they are served by the CDN
- Store data files on S3 instead of the local disk
- Store temporary files in /tmp
- Send background jobs to SQS
- Write logs to stderr so that they are automatically collected to AWS CloudWatch
It also provides helpers to reference assets served via the CDN, detect whether the app is running in Craft Cloud, and more.
## Conclusion
By building on top of cloud primitives, like AWS Lambda, SQS, S3, and Cloudflare workers, the Craft team is able to build a PaaS that is secure and scalable while keeping the maintenance effort manageable.
With this architecture, Craft Cloud has served over **310 million HTTP requests and 6TB of data** in the last 30 days.
Having been able to help the Craft team adopt Bref and AWS Lambda over the last few years, I am personally very excited to see the project go live, and I want to congratulate them for the launch! Running PHP online deserves to be simpler, and Craft is making it happen!
A huge thank you to the [Craft](https://craftcms.com/) team for being a long-term sponsor of Bref, and thank you [Jason](https://www.linkedin.com/in/jason-mccallister) for sharing that story with us!
---
Source: https://bref.sh/docs/case-studies/treezor
# Treezor: a serverless banking platform
This case study dives into how Treezor went serverless for their banking platform.
From legacy code running on servers to a serverless monolith, and then event-driven microservices on AWS with Bref.
[Treezor](https://www.treezor.com/) is a banking-as-a-service platform that serves millions of transactions every day. **You might be using it every day** through its clients: neobanks, employee benefit cards, company travel cards, and many other financial services.
Because Treezor's clients have very different use cases, the platform's infrastructure must be able **to scale and be resilient** to accommodate various usage patterns. Whether it's a luncheon voucher transaction spike at lunchtime or a monthly batch of transactions by corporate clients. On top of that, some API endpoints need to respond fast, **in near real-time**, for example to authorize live credit card payments.
To build such a platform, Treezor migrated from a legacy PHP application running on servers to a serverless architecture running on AWS Lambda with Bref. They did such a migration in 3 steps:
- First, they validated the serverless infrastructure by building a new service as a serverless PHP application.
- Then, they did a "lift-and-shift" migration by **running the legacy PHP application on AWS Lambda**.
- Finally, they slowly refactored the legacy application into multiple **PHP microservices** using the "strangler" pattern.
Let's explore this serverless migration in more detail.
## The original legacy stack
The original stack was a legacy PHP monolith, built with no framework, running on [OVH](https://ovh.com/) servers.
That monolith was responsible for the "Core Banking" API, i.e. handling all the critical operations of the system, like authorizing credit card payments or executing bank transfers.
Because it was running on servers, handling unpredictable traffic spikes was challenging.
## Validating the serverless infrastructure
Migrating a Core Banking system to a new infrastructure is not something you do every day. To make sure that AWS and [AWS Lambda](https://aws.amazon.com/lambda/) were a good fit, the team first wanted to validate the stack.
They did so by **building a new service entirely serverless** (greenfield project). They built it using PHP, Lumen, Bref, AWS Lambda, SQS, DynamoDB, SNS, S3, and KMS.
That new service was the API exposed to Treezor's client to manage day-to-day bank operations. It was deployed to production at the end of 2020 and was a success. It was able to scale and handle the incoming traffic.
To confirm that serverless worked well with other use cases, including the most complex ones, they did a second migration and **routed a part of live credit card transactions to the new serverless app**. If it worked with the live credit card traffic, it meant that the rest of the Treezor platform could run as serverless too.
That migration was also a success and cleared the way for migrating more APIs.
## Lift-and-shift the legacy monolith on AWS Lambda
The next step was to migrate the legacy PHP monolith from servers to AWS Lambda.
Doing [a complete rewrite was unrealistic](https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/). It would have meant completely halting all other developments, investing months (or even years) into the new system, and crossing fingers for the rewrite to actually be a success.
Instead, the team used Bref's [PHP-FPM runtime](https://bref.sh/docs/runtimes/fpm-runtime). This AWS Lambda runtime runs PHP "as usual", like on any server, using PHP-FPM. It allowed taking the monolithic codebase and running it as an HTTP application on AWS Lambda.
To do a controlled migration, the team deployed the application both to the servers and to AWS Lambda. **Only 10 lines of code needed to be changed** to run the monolith on AWS Lambda.
Using API Gateway, they were able to **route some of the API traffic to AWS Lambda and the rest to the old stack running on servers**. In case of any issue, it was possible to roll back the endpoint to the server stack.

In October 2021, they successfully migrated their first API route to AWS Lambda, the most critical one handling all live credit card transactions. Over the following year, more and more API endpoints were migrated away from the servers to AWS Lambda.
The migration of other API endpoints started in March 2022. Over the following year, the team migrated all API endpoints, cron tasks, and batch scripts to AWS Lambda. The servers were finally shut down in March 2023.
The entire migration took 1 year of planning and design, and 1 year of implementation. The main challenge was dealing with cron tasks running for more than 15 minutes (the maximum execution time on AWS Lambda). They needed to be split into smaller tasks. The Bref runtime also needed some customizations, for example to run custom PHP versions.
**The migration was a complete success**, and this is reflected in the key metrics tracked for the migration. With the new serverless stack:
- API response times were **2.5 times faster**.
- On-call alerts were **divided by 2** or even 3 times.
- API endpoint timeouts for card transactions were **reduced by a factor of 10**.
## Refactoring to serverless microservices
Now that the infrastructure was running smoothly, the team turned to the code itself. The goal: turning the legacy PHP monolith into a maintainable system.
Headcount in the Treezor IT department was growing and having all teams work on a single monolith was painful. The target architecture was set: a collection of **domain-oriented microservices**.
The use of API Gateway routing turned useful here too: it allowed applying the "strangler" pattern. As each domain was spun out into a separate service, the API Gateway routes could be transparently updated to point to the new services.

The migration to microservices is still an ongoing work. As of today, the stack is **90% serverless on AWS** and deployed with Terraform.
Some new services are implemented in Go, but a majority are using PHP with Bref. API Gateway is used for [HTTP APIs](https://bref.sh/docs/use-cases/http), and EventBridge is used for [asynchronous communication between services](https://bref.sh/docs/use-cases/eventbridge). Other AWS services are used inside services to handle specific use cases, for example [job queues with SQS](https://bref.sh/docs/use-cases/sqs), DynamoDB databases, SNS for parallelizing tasks, or even Kinesis for data pipelines.
## Conclusion
I find Treezor's story fascinating because it illustrates two very different use cases:
- Being able to **lift and shift** existing PHP applications to AWS Lambda with very few changes.
- And later going "all-in" on **event-driven microservices** and taking full advantage of what AWS has to offer.
It shows that both options are valid and have their own benefits.
First, **using AWS Lambda as a scalable PHP hosting platform works well**. For those who want scalability and simplicity, it is possible to avoid vendor lock-in and use AWS Lambda like any other hosting platform.
Second, AWS Lambda has great integrations with other AWS services. That allows building **event-driven microservices by composing the best AWS services for the use case**: SQS for infinitely scalable queues, EventBridge for asynchronous communication between services, DynamoDB for very optimized data storage, API Gateway for out-of-the-box caching, security and advanced routing for APIs... And much more of course.
Treezor's story also shows that the first option can be a good stepping stone to the second, removing some of the risk of a cloud migration.
Thank you [Treezor](https://www.treezor.com/), and thank you [Nicolas](https://www.linkedin.com/in/nicolasbordes/) and [Julien](https://www.linkedin.com/in/julien-mortuaire-29126528/) for sharing that story with us!
And if you want to work with Bref every day, [Treezor is hiring](https://www.welcometothejungle.com/fr/companies/treezor) 😉
---
Source: https://bref.sh/docs/upgrading/v2
# Upgrading to Bref 2.0
## Updating dependencies
### PHP 8 required
Bref 2.0 now requires PHP 8.0 or greater.
### Composer Dependencies
You should update the `bref/bref` dependency in your application's composer.json file:
```diff
- "bref/bref": "^1.0",
+ "bref/bref": "^2.0",
```
Then run `composer update bref/bref --with-all-dependencies`.
If you use the [Bref Extra extensions](https://github.com/brefphp/extra-php-extensions), you also need to update the `bref/extra-php-extensions` package to version `^1.0`.
### Serverless Framework
If you are using Bref with [Serverless Framework](https://www.serverless.com/framework) (which is the default), Bref 2.0 requires Serverless Framework v3. Serverless Framework 2.x is no longer supported.
To check your Serverless Framework version, run:
```bash
serverless --version
```
If you need to upgrade, [read the Serverless Framework documentation](https://www.serverless.com/framework/docs/getting-started#upgrade) (short version: run `npm install -g serverless@3`).
## PHP runtimes
There is a new (simpler) syntax to use Bref's PHP runtimes in `serverless.yml`:
```yaml
functions:
hello:
# ...
runtime: php-84
# instead of:
runtime: provided.al2
layers:
- ${bref:layer.php-84}
```
The [bref.sh](https://bref.sh) documentation now uses the simpler `runtime: php-84` syntax, but `${bref:layer.php-xxx}` variables still work! These variables are not deprecated. There are no breaking changes here.
## Bref CLI
The following commands of `vendor/bin/bref` have changed:
- `vendor/bin/bref cli` is replaced by the simpler `serverless bref:cli`.
For example:
```bash
vendor/bin/bref cli mystack-dev-artisan --region=eu-west-1 -- migrate --force
# becomes:
serverless bref:cli --args="migrate --force"
```
No need to provide the function name or the region anymore. Read [the Console documentation](https://bref.sh/docs/runtimes/console#usage) to learn more. You will also find alternatives if you don't use the `serverless` CLI.
- `vendor/bin/bref local` is replaced by the simpler `serverless bref:local`.
For example:
```bash
vendor/bin/bref local --handler=my-handler.php
# becomes:
serverless bref:local -f hello
```
No need to provide the handler file name anymore, we directly use the function name. The new `serverless bref:local` command has similar arguments as `serverless invoke`.
Read [the Local Development documentation](https://bref.sh/docs/local-development/event-driven-functions) to learn more. You will also find alternatives if you don't use the `serverless` CLI.
- `vendor/bin/bref layers` is replaced by the simpler `serverless bref:layers`.
Layer versions are also available at [runtimes.bref.sh](https://runtimes.bref.sh/) if you don't use the `serverless` CLI.
These changes allowed us to simplify the commands (automatically use the AWS region, credentials and stage from the `serverless` CLI). It also allowed us to remove the biggest `bref/bref` Composer dependencies and make the package much lighter.
## Development Docker images
The development Docker images have been simplified. You will need to update your `docker-compose.yml`.
Before (Bref v1):
```yaml
services:
web:
image: bref/fpm-dev-gateway
ports:
- '8000:80'
volumes:
- .:/var/task
depends_on:
- php
environment:
HANDLER: public/index.php
DOCUMENT_ROOT: public
app:
image: bref/php-80-fpm-dev
volumes:
- .:/var/task
console:
image: bref/php-80
volumes:
- .:/var/task
entrypoint: php
```
After (Bref v2):
```yaml
services:
app:
image: bref/php-80-fpm-dev:2
ports: [ '8000:8000' ]
volumes:
- .:/var/task
environment:
HANDLER: public/index.php
DOCUMENT_ROOT: public
```
The `bref/php-XX-fpm-dev` images can now run HTTP applications, console commands as well as event-driven functions too. Read more in [web app local development](https://bref.sh/docs/local-development).
The `bref/fpm-dev-gateway` image is no longer needed, and code running in `bref/php-XX-fpm-dev` now runs in an environment even closer to production.
> Note: In order to be future-proof, we recommend to set the Bref major version in the Docker images. For example, with Bref 2, use `bref/php-80-fpm-dev:2` instead of just `bref/php-80-fpm-dev`.
## Smaller breaking changes that might impact you
The changes below should not impact the majority of users. However, if you are using any of these features, you might need to update your code.
### Removed `separateVendor: true`
The `separateVendor` option (in `serverless.yml`) has been removed.
It allowed to deploy the `vendor/` directory separately from the app to reduce the size of the deployment package. It was very buggy (contributed a long time ago and not maintained by anyone) and added a lot of technical debt.
Instead, when hitting the 250MB limit, you are encouraged to [deploy via Docker images](https://bref.sh/docs/web-apps/docker.html) instead (which have a limit of 10GB).
### Removed `Bref\Lambda\SimpleLambdaClient`
The `Bref\Lambda\SimpleLambdaClient` class has been removed. It was a simple wrapper around the AWS SDK that was used internally in Bref.
If you were using this class in your application, you can use the AWS SDK directly, or the simpler and lighter [Async AWS package](https://async-aws.com/clients/lambda.html).
### Removed `Bref\Websocket\SimpleWebsocketClient`
The `Bref\Websocket\SimpleWebsocketClient` class has been extracted to a separate package: [bref/api-gateway-websocket-client](https://github.com/brefphp/api-gateway-websocket-client).
### Changed the AWS account ID for AWS Lambda layers
The AWS account ID where Bref layers are published is different for v2. That lets us keep releasing Bref v1 layers without mixing up layer numbers (e.g. layer 24 being a v2 layer and layer 25 being a v1 layer).
For Bref v2 layers, you need to use `534081306603` as the AWS account number (instead of `209497400698`).
```bash
# Bref v1 layer
arn:aws:lambda:eu-west-2:209497400698:layer:php-82-fpm:xxx
# Bref v2 layer
arn:aws:lambda:eu-west-2:534081306603:layer:php-82-fpm:xxx
```
---
Source: https://bref.sh/docs/upgrading/v3
# Upgrading to Bref 3.0
Read the [Bref 3.0 release announcement](https://bref.sh/news/03-bref-3.0) to learn about all the new features and improvements.
## Updating dependencies
### PHP 8.2 required
Bref 3.0 now requires PHP 8.2 or greater.
### Composer Dependencies
Update all Bref packages in your `composer.json` file from `^2` to `^3`, for example:
```json filename="composer.json"
"require": {
"bref/bref": "^3",
"bref/laravel-bridge": "^3", // if you use Laravel
"bref/symfony-bridge": "^3", // if you use Symfony
"bref/extra-php-extensions": "^3" // if you use extra extensions
}
```
> [!TIP]
>
> Only update the versions for the packages you actually have in your project.
> [!TIP]
>
> If you use [`bref/extra-php-extensions`](https://github.com/brefphp/extra-php-extensions), note that it jumps from v1 to v3 (there is no v2). The v1 version was used with Bref v2, and v3 is aligned with Bref v3.
Then run:
```bash
composer update --with-all-dependencies
```
## PHP extension changes
The following improvements in Bref 3.0 let you clean up old configurations.
### PostgreSQL extension is enabled by default
The PostgreSQL PDO extension (`pdo_pgsql`) is now enabled by default in Bref layers.
If you had enabled it manually via a `php.ini` file, you can remove that line:
```diff filename="php/conf.d/php.ini"
-extension=pdo_pgsql
```
### Redis extension is now built-in
The Redis PHP extension is now included in Bref layers by default.
If you were using the Redis extension from [bref/extra-php-extensions](https://github.com/brefphp/extra-php-extensions), remove the layer from your `serverless.yml`:
```diff filename="serverless.yml"
functions:
api:
# ...
layers:
- - ${bref-extra:redis-php-84}
```
And enable the extension via a `php.ini` file in your project (`php/conf.d/php.ini`):
```diff filename="php/conf.d/php.ini"
extension=redis
```
If Redis was the only `bref/extra-php-extensions` extension you were using, you can uninstall the package:
```bash
composer remove bref/extra-php-extensions
```
## Container image changes
If you deploy using [container images](https://bref.sh/docs/deploy/docker), you must update your `Dockerfile`.
**If you don't have a `Dockerfile` in your project, you can skip this section.**
Since Bref container images have been merged into a single `bref/php-xx` image, the following images don't exist anymore: `bref/php-xx-fpm` and `bref/php-xx-console`.
### Option 1: Set BREF_RUNTIME in the Dockerfile
The simplest upgrade path is to update your Dockerfile to use the unified image and set the runtime:
```diff filename="Dockerfile"
- FROM bref/php-84-fpm:2
+ FROM bref/php-84:3
+ ENV BREF_RUNTIME=fpm
# ...
```
Replace `fpm` with `function` or `console` depending on your use case.
### Option 2: Use one image for all function types (recommended)
A major benefit of v3 is that you can now use **a single Docker image** for all your functions (web, console, queues, etc.) and set `BREF_RUNTIME` per function in `serverless.yml`.
**Dockerfile** (single image for everything):
```dockerfile filename="Dockerfile"
FROM bref/php-84:3
# Your application code
COPY . /var/task
# No BREF_RUNTIME set here!
```
**serverless.yml** (set runtime per function):
```yml filename="serverless.yml"
functions:
web:
image:
name: my-app-image
environment:
BREF_RUNTIME: fpm # Web/HTTP function
events:
- httpApi: '*'
console:
image:
name: my-app-image
environment:
BREF_RUNTIME: console # Console commands
worker:
image:
name: my-app-image
environment:
BREF_RUNTIME: function # Queue worker
events:
- sqs:
arn: !GetAtt MyQueue.Arn
```
This approach lets you build and deploy a single Docker image for all function types, simplifying your deployment pipeline.
### Local development images
If you use the `bref/php-84-fpm-dev` image for local development, update it to:
```diff filename="docker-compose.yml"
- FROM bref/php-84-fpm-dev:2
+ FROM bref/php-84-dev:3
```
You can then set `BREF_RUNTIME` environment variable in your `docker-compose.yml` file or in the `Dockerfile` directly.
## Smaller breaking changes that might impact you
The changes below should not impact the majority of users. However, if you are using any of these features, you might need to update your code.
### Changed the AWS account ID for AWS Lambda layers
The AWS account ID where Bref layers are published is different for v3. That lets us keep releasing Bref v2 layers without mixing up layer numbers. If you reference the layers via their full ARN, you must update the Bref AWS account number to `873528684822` (instead of `534081306603`).
```bash
# Bref v2 layer
arn:aws:lambda:us-east-1:534081306603:layer:php-84:xxx
# Bref v3 layer
arn:aws:lambda:us-east-1:873528684822:layer:php-84:xxx
```
**If you don't know what that means**, you're likely not concerned by this change.
If you're not sure, search for `534081306603` in your codebase and replace it with the new account ID.
### AWS Lambda layers have been merged into a single layer
If you configure the `runtime` in your functions using the following syntax:
```yml filename="serverless.yml"
functions:
web:
# ...
runtime: php-84-fpm # or `php-xx` or `php-xx-console`
```
✅ **you have nothing to do**, your configuration is valid for Bref v3.
However, if you specify AWS Lambda layers explicitly in `serverless.yml` (or through any other deployment method), for example:
```yml filename="serverless.yml"
functions:
web:
# ...
runtime: provided.al2
layers:
- ${bref:layer.php-84}
# or:
- ${bref:layer.php-84-fpm}
# or:
layers:
- 'arn:aws:lambda:us-east-1:534081306603:layer:php-84:21'
```
Then you must update your configuration.
Under the hood, **all layers have been merged into one**, i.e. `php-xx-fpm` and `php-xx-console` have been merged into `php-xx` to make Bref layers simpler. The runtime is now defined via an environment variable that is automatically injected by Bref when using the `runtime:` syntax.
- **Option 1**: switch to using the simpler `runtime:` syntax.
Before:
```yml filename="serverless.yml"
functions:
web:
# ...
runtime: provided.al2
layers:
- ${bref:layer.php-84}
# or:
- ${bref:layer.php-84-fpm}
# or:
- ${bref:layer.php-84-console}
```
After:
```yml filename="serverless.yml"
functions:
web:
# ...
runtime: php-84
# or:
runtime: php-84-fpm
# or:
runtime: php-84-console
```
The examples above assume you are using PHP 8.4 (`php-84`) but you can replace `84` with another PHP version.
If you include additional layers, you can keep them without issues, for example:
```yml filename="serverless.yml"
functions:
web:
# ...
runtime: php-84-fpm
layers:
- ${bref-extra:imagick-php-84}
```
- **Option 2**: change the layer names and define the `BREF_RUNTIME` environment variable.
Before:
```yml filename="serverless.yml"
functions:
web:
# ...
runtime: provided.al2
layers:
- ${bref:layer.php-84}
# or:
- ${bref:layer.php-84-fpm}
# or:
- ${bref:layer.php-84-console}
```
After:
```yml filename="serverless.yml"
functions:
web:
# ...
runtime: provided.al2
layers:
- ${bref:layer.php-84}
environment:
# ...
BREF_RUNTIME: function # for ${bref:layer.php-xx}
# or
BREF_RUNTIME: fpm # for ${bref:layer.php-xx-fpm}
# or
BREF_RUNTIME: console # ${bref:layer.php-xx-console}
```
The examples above assume you are using PHP 8.4 (`php-84`) but you can replace `84` with another PHP version.
### The `vendor/bin/bref` CLI has been removed
The `vendor/bin/bref` CLI has been completely removed in Bref 3.0. This is a minor change since the CLI was already 90% removed in Bref 2.0 - only the `bref init` command remained for bootstrapping new projects.
We now have better onboarding with improved documentation. Here are the alternatives:
- **Scaffolding new projects**: Follow the [getting started guide](https://bref.sh/docs/setup) to create your `serverless.yml` manually.
- **Layer versions**: Visit [runtimes.bref.sh](https://runtimes.bref.sh/) or run `serverless bref:layers`.
- **Running console commands**: Use `serverless bref:cli` (unchanged from v2).
- **Local development**: Use [Docker-based local development](https://bref.sh/docs/local-development).
### The hooks system has been removed
The deprecated hooks system has been removed. This change affects very few users (less than 1%) as it was a low-level API used primarily by framework integrations.
If you were using `Bref::beforeStartup()` or `Bref::beforeInvoke()`, you must migrate to the `BrefEventSubscriber` pattern:
Before:
```php
use Bref\Bref;
Bref::beforeStartup(function () {
// Setup code
});
```
After:
```php
use Bref\Bref;
use Bref\Listener\BrefEventSubscriber;
class MyEventSubscriber extends BrefEventSubscriber
{
public function beforeStartup(): void
{
// Setup code
}
}
Bref::events()->subscribe(new MyEventSubscriber());
```
The `BrefEventSubscriber` class provides additional hooks: `afterStartup()`, `beforeInvoke()`, and `afterInvoke()`. This refactor powers better integrations like the [Laravel bridge](https://github.com/brefphp/laravel-bridge), [X-Ray integration](https://bref.sh/xray), and [Sentry integration](https://bref.sh/sentry).
### SOAP extension is now disabled by default
The SOAP PHP extension is now disabled by default. It had very little usage, and disabling it helped reduce layer sizes to make room for more commonly used extensions.
If you need the SOAP extension, you can enable it by creating a `php.ini` file in your project:
```ini filename="php/conf.d/soap.ini"
extension=soap
```
### Laravel version compatibility
If you are using Laravel with Bref, note that:
- **Laravel 10, 11, or 12 is required** (Laravel 8 and 9 are no longer supported).
- Update `bref/laravel-bridge` to the latest version.
The Laravel bridge has been updated to use the new `BrefEventSubscriber` pattern internally, but this change is transparent to users.
### CloudWatch log formatter enabled by default
The [Bref Monolog formatter](https://github.com/brefphp/monolog-bridge) is now enabled by default in the Laravel and Symfony bridges.
**This changes how your logs will look.** Logs now use a hybrid format that combines human-readable text with structured JSON:
Before (plain text):
```
[2025-12-05 10:30:45] production.ERROR: Database connection failed
```
After (structured format):
```
ERROR Database connection failed {"message":"Database connection failed","level":"ERROR","context":{...}}
```
This format makes logs easier to read in CloudWatch and enables powerful filtering with CloudWatch Logs Insights (e.g., filter by log level or exception class).
**Exception handling is greatly improved:** Previously, exception stack traces were split across multiple CloudWatch log records (one per line), making them difficult to read and browse. Now, the entire exception (including stack trace) is grouped in a single JSON object, making debugging much easier.
If you prefer the old format, you can disable the formatter in your Laravel or Symfony configuration. See the [Bref Monolog documentation](https://github.com/brefphp/monolog-bridge) for details.
### Locale files from glibc langpacks are no longer included
If you are using [`intl` / ICU](https://www.php.net/manual/en/book.intl.php) for locale-aware formatting (dates, numbers, currencies) or translations, you don't need to do anything as ICU is fully supported in Bref v3. **Modern frameworks (Laravel, Symfony…) use ICU, not `gettext`**. That means most users are not affected.
If you are using PHP's native [`gettext`](https://www.php.net/manual/en/book.gettext.php) functions or rely on `setlocale()` with specific locales (e.g. for
`strftime()`), glibc locale files are no longer included in Amazon Linux 2023 (AL2023). These will no longer work in Bref v3. You may either:
- Switch to using `intl` / ICU, which is the modern standard for translations in PHP and is fully supported in Bref v3.
- Or, if you want to keep using `gettext` or `setlocale()`, switch to [container deployments](https://bref.sh/docs/deploy/docker) and use a custom Docker image that includes the necessary locale files:
```dockerfile filename="Dockerfile"
# Install the locale packages you need (e.g. English and French)
RUN microdnf update -y && \
microdnf install -y glibc-langpack-en glibc-langpack-fr && \
microdnf clean all
```
---
That's it! Read the [Bref 3.0 release announcement](https://bref.sh/news/03-bref-3.0) to learn more about all the new features and improvements in this release.
---
Source: https://bref.sh/docs/cloud
# Bref Cloud documentation
[Bref Cloud](https://bref.sh/cloud) is the service that complements the open-source Bref project. It deploys, monitors and operates serverless PHP applications on AWS Lambda, in your own AWS account, without the AWS complexity.
To get started, [create a Bref Cloud account](https://bref.cloud/register). There is a free plan for personal projects, and paid plans come with a free trial. See the [pricing](https://bref.sh/cloud#pricing).
} title="Learn more about Bref Cloud" href="https://bref.sh/cloud" />
} arrow={true} title="Get started with Bref Cloud" href="https://bref.cloud/register" />
## What Bref Cloud does
### Deploy
- **One command**: `bref deploy` from your machine or [from GitHub Actions](https://bref.sh/docs/cloud-deploy). No AWS credentials to create, distribute or rotate: Bref Cloud generates [short-lived credentials](https://bref.sh/docs/cloud-security) for every deployment.
- **Databases and networks**: create MySQL or PostgreSQL databases (fixed instances, or serverless databases that scale and pause on their own) and private networks in a few clicks.
- **Environments**: manage dev, staging and production across AWS accounts and regions from one dashboard, with the history of every deployment (also available from the terminal with [`bref deployments`](https://bref.sh/docs/cloud-deploy#deployment-history)).
### Monitor
- **Overview**: a diagram of each environment's architecture with live metrics on every component.
- **Logs**: search logs or tail them in real time in the dashboard, or search them from the terminal with [`bref logs`](https://bref.sh/docs/environment/logs#reading-logs).
- **Metrics**: Lambda invocations, concurrency, duration and errors, API Gateway, queues and dead-letter queues.
- **Traces**: a trace explorer optimized for PHP applications, with a free [Bref X-Ray](https://bref.sh/xray) license included. See database queries, HTTP calls and AWS calls inside each invocation, and filter traces by route, job, command, cold start, or your own annotations, over up to 30 days.
- **Performance**: the slowest database queries, the latency of every route, and the slowest jobs of your application, computed from the last 30 days of traces.
- **Health checks**: verify that the database, the cache and the Lambda configuration of a deployed application are healthy (Laravel only for now).
[Learn more about monitoring with Bref Cloud](https://bref.sh/docs/monitoring#bref-cloud).
### Operate
- **Queues**: watch queues and dead-letter queues, and retry, delete or flush failed Laravel jobs.
- **Commands**: run console commands (Artisan, Symfony console…) on a deployed environment from the dashboard.
- **Secrets**: create and manage secrets for all your applications.
- **Files**: browse, upload, rename and delete files in S3 buckets.
### AI agents
AI agents like Claude Code, Cursor or Codex can read the logs and the deployments of your applications with the `bref` CLI: `bref logs`, `bref deployments`, `bref deployments:show` and `bref deployments:logs`. For example, an agent can look for the error behind a bug report, or find why a deployment failed.
The CLI detects when an agent runs it and adapts its output: no colors or spinners, logs as compact text, and deployments as JSON.
### Team and security
- **Team members**: invite teammates with read-only, write or admin access. They never need AWS access.
- **AWS access**: Bref Cloud connects to your AWS accounts through IAM roles and temporary credentials, never long-lived access keys. [Learn more](https://bref.sh/docs/cloud-security).
---
Source: https://bref.sh/docs/cloud-getting-started
# Getting started with Bref Cloud
Bref Cloud is a service that makes it easy to deploy and monitor serverless PHP applications on AWS Lambda. Learn more about it [on the Bref Cloud homepage](https://bref.sh/cloud).
To get started, [create a free Bref Cloud account](https://bref.cloud/register).
You will be guided through the process of creating an AWS account (if needed) and connecting it to Bref Cloud. If you create a new AWS account, use the [AWS sign-up form](https://signin.aws.amazon.com/signup?request_type=register) (*Sign up for AWS (advanced)*). Do not use *Sign up for AWS (new)*: it creates an [AWS project](https://bref.sh/docs/setup#aws-projects), a restricted AWS account that is not suited for production applications, and that Bref Cloud cannot connect to.
## Installing the CLI
To deploy PHP applications using Bref Cloud, you need to install the Bref CLI:
```shell
composer global require bref/cli
```
The [`global` option](https://getcomposer.org/doc/03-cli.md#global) ensures that the `bref` CLI is installed globally on your machine in the `~/.composer/vendor/bin` directory.
Make sure the command works by running:
```shell
bref --version
```
If the `bref` command is not found, you need to add Composer's global bin directory to your `PATH`. Add this line to your shell configuration file (e.g. `~/.bashrc`, `~/.zshrc`, `~/.profile`):
```shell
export PATH="$PATH:$HOME/.composer/vendor/bin"
```
If you want to find the location of the Composer's global installation directory, run `composer -n config --global home`.
> [!TIP]
>
> To keep the `bref` CLI up-to-date, run `composer global update bref/cli`.
## Connecting the CLI
To connect the CLI to your Bref Cloud account, run:
```shell
bref login
```
This command will open a browser window where you can log in to your Bref Cloud account and authorize the CLI to access your account.
When running in a non-interactive environment (e.g. CI/CD, or a Docker container), you can set a `BREF_TOKEN` environment variable instead. You can create a token in the [Bref Cloud dashboard](https://bref.cloud):
- either as a [personal token](https://bref.cloud/user/api-tokens), which makes the CLI act on your behalf, with your permissions
- or as a "bot token", which represents a service or a CI/CD pipeline that is not tied to a specific user
---
Source: https://bref.sh/docs/cloud-deploy
// Path relative to the copy in the `website/` folder
# Deployment
Bref Cloud deploys applications using Bref's `serverless.yml` file.
If you haven't gotten started with Bref, follow the guide corresponding to your framework:
} title="Get started with Laravel" arrow="true" href="https://bref.sh/docs/laravel/getting-started" />
} title="Get started with Symfony" arrow="true" href="https://bref.sh/docs/symfony/getting-started" />
The main difference with Bref Cloud is that you don't deploy by running `serverless deploy`. Instead, you deploy by running:
```shell
bref deploy
```
(see [the Getting Started guide](https://bref.sh/docs/cloud-getting-started) to install the `bref` CLI)
The benefits of deploying using Bref Cloud:
- **Credentials**: You don't need to create AWS credentials or configure the `serverless` CLI, Bref Cloud handles this for you ([learn more](https://bref.sh/docs/cloud-security)).
- **Deployment monitoring**: Bref Cloud provides a dashboard to view deployments of all your applications across all AWS accounts, regions, and environments.
- **No need to install `serverless`**: Bref Cloud transparently installs and runs the `serverless` CLI.
- **Better UX**: the `bref` CLI has minimal output with verbose mode that can be enabled in real time.
When you run `bref deploy`, Bref Cloud runs the `serverless deploy` command for you in the background.
## Differences between `bref` and `serverless`
The `bref` CLI uses AWS credentials from AWS accounts connected in [Bref Cloud](https://bref.cloud/aws-accounts). That means that it will ignore AWS credentials configured on your machine.
The `bref` CLI also uses the [open-source fork of Serverless Framework](https://github.com/oss-serverless/osls), which is a fork of Serverless Framework v3, the last free and open-source version. This fork is maintained by Bref and ensures that the CLI is maintained and stable in the long run.
Finally, the `bref` CLI uses "**environments**" instead of "**stages**". This is just a vocabulary change. That means that you should use the `--env` option instead of the `--stage` option:
```shell
bref deploy --env production
# is equivalent to
serverless deploy --stage production
```
### Parameters
To set [Serverless Framework parameters](https://www.serverless.com/framework/docs/guides/parameters) (`${param:...}` variables in `serverless.yml`) when deploying, use the `--param` option, like with `serverless deploy`. It can be repeated:
```shell
bref deploy --param="domain=example.com" --param="queue=orders"
```
## Deploying with Bref Cloud
To deploy an application using Bref Cloud, you need to add your Bref Cloud team to the `serverless.yml` file:
```yaml
service: my-app
provider:
name: aws
# ...
# Add these lines:
bref:
team: my-bref-cloud-team
```
(you can find your team name [in Bref Cloud](https://bref.cloud/app/create))
Then, you can deploy using the `bref` CLI:
```shell
bref deploy
# or for a specific environment
bref deploy --env production
```
## Deployment history
Bref Cloud keeps the history of the deployments of every environment, with their logs. You can see it in the dashboard, or from the terminal:
```shell
# The latest deployments of an environment
bref deployments --env=production
# The status, git commit, author and error of the latest deployment
bref deployments:show --env=production
# The logs of the latest deployment, for example to find why it failed
bref deployments:logs --env=production
# A specific deployment, by the ID that `bref deployments` shows
bref deployments:show 42
bref deployments:logs 42
```
When given a deployment ID, these commands work from any directory. Otherwise, they need the project's configuration file, or the `--app` and `--team` options. Add `--json` for machine-readable output.
## Deploying from GitHub Actions
You can also deploy your application using GitHub Actions.
First, make sure you have set up the `bref.team` option in `serverless.yml` as shown in the section above.
Then, create a `.github/workflows/deploy.yml` workflow. Here is an example workflow you can use as a starting point:
```yaml
name: Deploy
on:
push:
branches: [ main ]
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency: deploy-prod # Avoid deploying concurrently
environment:
name: prod
# Optionally set a URL for the environment
# url: https://example.com
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Cache NPM dependencies
uses: actions/cache@v4
with:
path: ~/.npm # npm cache files are stored in `~/.npm`
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-node-
# You can remove this step if you don't have a `package.json` file
- run: npm ci
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
# Install the Bref CLI as a global tool
tools: bref/cli
- uses: ramsey/composer-install@v3
with:
composer-options: '--optimize-autoloader --no-dev'
# You may need to build your frontend application here
# - run: npm run build
- run: bref deploy --env=prod
env:
BREF_TOKEN: ${{ secrets.BREF_TOKEN }}
```
> [!TIP]
>
> Depending on your framework, you may need to add extra steps to build your application. For example with Laravel or Symfony you may want to build assets before deploying.
Finally, you need to add the `BREF_TOKEN` secret to your repository:
- [Create a "bot token" in your Bref Cloud team](https://bref.cloud/)
- Go to your repository on GitHub
- Click on "Settings" > "Secrets and variables" > "Actions" > "New repository secret"
- Set the name to `BREF_TOKEN` and the value to the token you received after creating the bot token

That's it! Your application will be deployed automatically when you push to the main branch. On the first deployment, the application will appear in the Bref Cloud dashboard.
---
Source: https://bref.sh/docs/cloud-security
# Secure access to your AWS accounts
Bref Cloud accesses your AWS accounts to deploy and manage resources, such as AWS Lambda, S3 buckets, etc. To do this, it interacts with AWS via its SDK and API.
Instead of using long-lived AWS credentials (access keys), Bref Cloud uses IAM roles with temporary credentials following AWS best practices. This document explains how Bref Cloud accesses these AWS accounts securely in more detail.
## Introduction
```mermaid
graph LR
Users -->|web UI| cloud[https://bref.cloud];
bref[bref CLI] -->|API| cloud;
cloud -->|AWS API| user[User's AWS Account];
```
"Accessing an AWS account" means that Bref Cloud performs actions in the account, like creating a Lambda function or an S3 bucket. Practically, this is done via the AWS API using the AWS SDK.
To use the AWS API, Bref Cloud needs **AWS credentials** that give access to the target account.
Bref Cloud does not use long-lived AWS credentials (aka AWS access keys). Instead, it follows standard AWS best practices and uses IAM roles with [temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#bp-workloads-use-roles).
Note that all of this happens automatically and is easy to set up in the Bref Cloud web interface.
## How it works
```mermaid
graph LR
subgraph Bref AWS account
direction LR
cloud[bref.cloud] -->|AssumeRole| accessor[BrefCloudAccessor IAM role];
end
subgraph User's AWS account
direction LR
accessor -->|AssumeRole| accessRole[BrefCloudAccess IAM role];
accessRole --> resources["AWS resources (Lambda, etc.)"];
end
```
When you connect an AWS account to Bref Cloud, Bref Cloud creates a `BrefCloudAccess` IAM role in your account (you can also create it manually if you prefer). This role:
- gives permissions to access specific resources in your account (like Lambda functions)
- can only be "assumed" by the `BrefCloudAccessor` role from the Bref Cloud AWS account (called a "trust policy").
When Bref Cloud needs to access your account, it:
1. assumes the `BrefCloudAccessor` role in its own AWS account
2. uses this new role to assume the `BrefCloudAccess` role in your AWS account
3. gets temporary credentials that have the permissions of the `BrefCloudAccess` role
These credentials are valid for a short period of time. Bref Cloud uses them to perform the actions you requested (like retrieving logs). They are also passed to the `serverless deploy` command when deploying your application, so that the command uses these temporary credentials without you having to do anything.
Bref Cloud does not store these credentials. They are only used for the duration of the operation.
### Why use IAM roles?
IAM roles are the recommended way to grant access to AWS resources over long-lived credentials (like access keys). Access keys can be leaked and are hard to rotate. IAM roles can only be used by the Bref AWS account.
Thanks to IAM roles, **Bref Cloud does not store AWS credentials**.
You can also transparently review the permissions that Bref Cloud has in your account, adjust them (the IAM role lives in your account), or revoke access at any time by deleting the `BrefCloudAccess` role from your account.
### Why use a separate `BrefCloudAccessor` role?
The `BrefCloudAccessor` role is an additional layer of security: instead of directly granting the PHP application (Bref Cloud) access to your account, it jumps through an intermediate role (`BrefCloudAccessor`).
The reasons for this are:
- We can very tightly restrict access to this role (principle of least privilege). For example Bref AWS account administrators (IAM users) do not have access to the `BrefCloudAccessor` role.
- The PHP application is deployed often, but the `BrefCloudAccessor` role should almost never change. Separating those helps avoid accidental unwanted changes.
### What permissions does the `BrefCloudAccess` role have?
By default, the `BrefCloudAccess` role has a set of permissions that allows Bref Cloud to deploy and manage your applications. For example, it can create AWS resources (like Lambda functions) via CloudFormation, read logs and metrics, etc. When you set up Bref Cloud, you can review these permissions in the IAM role that is created in your account.
You can also customize the permissions of the `BrefCloudAccess` role if you want to restrict it further. Of course, all Bref Cloud features may not work correctly in some cases if you restrict the permissions too much.
### How does Bref Cloud know which role to assume?
When you connect your AWS account to Bref Cloud, the ARN of the `BrefCloudAccess` role is stored in Bref Cloud. This is how Bref Cloud knows which role to assume in your account.
### A note about `ExternalId`
Bref Cloud follows AWS best practices and uses the [`ExternalId` feature for cross-account access](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html).
The `ExternalId` is a unique identifier that is passed by Bref Cloud when assuming the role. This identifier is configured when the role is created, and is checked by AWS when the role is assumed.
External IDs are randomly generated *per Bref Cloud team* and are stored encrypted in the Bref Cloud database.
This technique has the following benefits:
- It ensures that only a specific Bref Cloud team can assume the role and access your account (protection against the "confused deputy" problem).
- In case of an extreme security breach where the `BrefCloudAccessor` IAM role is compromised, the attacker would not be able to access customer AWS accounts (the `ExternalId` acts as an additional layer of security).
- On top of that, the `ExternalId` being stored in the database means that the attacker would need to also compromise the database to get it, which is an additional layer of security.
- On top of that, the `ExternalId` being stored encrypted means that the attacker would also need to compromise the encryption key to get it (stored separately from the code and the database), which is another additional layer of security.
Note that nothing is foolproof, but having multiple layers of security helps mitigate risks.
## Self-hosted Bref Cloud
Self-hosting means running Bref Cloud inside your own AWS account. This is useful for companies that have strict security requirements and do not want to give access to a third-party service to their AWS account.
When you self-host Bref Cloud, cross-account access happens as usual but the big difference is that access happens from an AWS account that you own to another AWS account that you own.
```mermaid
graph LR
subgraph Your Organization
direction LR
subgraph "Your Bref Cloud AWS account"
cloud[Self-hosted bref.cloud] -->|AssumeRole| accessor[BrefCloudAccessor IAM role];
end
subgraph "Your Application AWS account"
accessor -->|AssumeRole| accessRole[BrefCloudAccess IAM role];
accessRole --> resources["AWS resources"];
end
end
```
## Frequently Asked Questions
### Can I revoke Bref Cloud's access to my AWS account?
Yes, you can revoke access at any time by deleting the `BrefCloudAccess` IAM role from your AWS account.
### How does Bref Cloud handle multi-account deployments?
If you need to deploy to multiple AWS accounts, you can connect each account to Bref Cloud separately. Each account will have its own `BrefCloudAccess` IAM role with appropriate permissions.
### How often are temporary credentials refreshed?
Temporary credentials are only used for the duration of specific operations and are not stored. They typically have a short lifespan (usually 15 minutes), or even just the duration of an HTTP request.
### Can I audit what actions Bref Cloud performs in my account?
You can enable [AWS CloudTrail](https://aws.amazon.com/cloudtrail/) to log all actions made in your AWS account. This allows you to audit the actions performed by Bref Cloud.
### Can I customize the permissions granted to Bref Cloud?
Yes, you can modify the permissions in the `BrefCloudAccess` IAM role to restrict what Bref Cloud can do in your AWS account. However, restricting permissions too much may prevent certain Bref Cloud features from working correctly. Feel free to reach out to Bref Cloud support if you need help with this.
### Does Bref Cloud comply with my organization's security requirements?
Bref Cloud follows AWS security best practices, including the principle of least privilege, temporary credentials, and cross-account access controls. For organizations with specific compliance requirements (like SOC2, HIPAA, etc.), you may want to consider the self-hosted option for complete control over the infrastructure.