Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Dynamodb Laravel Package

kitar/laravel-dynamodb

Seamlessly integrate Amazon DynamoDB into Laravel with Eloquent-style models, a fluent query builder, and local DynamoDB support for development and testing. Ideal for scalable, serverless-friendly NoSQL applications.

View on GitHub
Deep Wiki
Context7

Getting Started

kitar/laravel-dynamodb is the actively maintained fork of the older baopham/laravel-dynamodb. It gives you an Eloquent-style model and query builder on top of the AWS SDK's DynamoDbClient, so you can write User::find('u1') and User::where('id', '=', 'u1')->get() against DynamoDB tables. Most of the search traffic for this package lands on the archived baopham README, so treat that as the primary reference and the kitar repo as the current source of truth.

Install and publish config

composer require kitar/laravel-dynamodb
php artisan vendor:publish --tag=config   # writes config/dynamodb.php

Fill in the connection — either from standard AWS env vars or the package config:

// config/dynamodb.php
return [
    'key'      => env('AWS_ACCESS_KEY_ID'),
    'secret'   => env('AWS_SECRET_ACCESS_KEY'),
    'region'   => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'endpoint' => env('DYNAMODB_ENDPOINT'), // set for DynamoDB Local
    'token'    => env('AWS_SESSION_TOKEN'),
];

Define your first model

use Kitar\Dynamodb\Model;

class User extends Model
{
    protected $table      = 'users';
    protected $primaryKey = 'id';           // hash key
    protected $sortKey    = 'created_at';   // range key (optional)
    protected $keyType     = 'S';
    protected $sortKeyType = 'N';
    protected $fillable    = ['id', 'created_at', 'email', 'name'];
}

First useful case: a session / profile lookup by key

User::create([
    'id'         => 'u_123',
    'created_at' => now()->timestamp,
    'email'      => '[email protected]',
    'name'       => 'Ada',
]);

$user = User::find(['u_123', 1717000000]); // composite key: [hash, range]

If the table doesn't exist yet, create it outside of Laravel — the package does not ship migrations or a table builder. Use the AWS Console, aws dynamodb create-table, Terraform, or CloudFormation.

Local dev: run amazon/dynamodb-local in Docker, point DYNAMODB_ENDPOINT=http://localhost:8000, and use dummy credentials. AWS_SDK will happily ignore them against a local endpoint.

Implementation Patterns

Composite keys and lookups

  • $primaryKey must match the partition (hash) key attribute name.
  • $sortKey is optional; it enables range queries and indexed order.
  • find() takes a scalar for a hash-key-only table and an array [$hash, $range] for composite keys. If you get "ValidationException: The provided key element does not match the schema", you've mismatched one of these.

Querying: partition key first, then sort key

// hash-key equality is required to be a Query, not a Scan
$result = User::where('id', '=', 'u_123')
    ->where('created_at', '>', 1716000000)
    ->where('created_at', '<', 1719000000)
    ->orderBy('created_at', 'desc')   // maps to ScanIndexForward = false
    ->limit(25)
    ->get();

Additional where() clauses on non-key attributes become a FilterExpression. That is still useful — filters run after the read and reduce what's returned — but they do not reduce consumed read capacity and do not turn a partition scan into a cheap lookup. Design the key (or a GSI) around the access pattern you actually need.

Secondary indexes

Select the index on the builder before the key conditions; the package exposes an index selector (see the README for the exact method name in your version). Everything else behaves like the base table, with the caveat that GSIs are eventually consistent.

Pagination and scanning

$page = User::where('id', '=', 'u_123')->paginate(20);

paginate() uses ExclusiveStartKey internally, which is why it works — but it does not know the total row count without a Scan. Avoid count() in listings and UI badges; cache a separately maintained counter or accept "next page" style pagination. For full-table work, use Model::scan() and chunk it rather than ->get() on an unbounded scan.

Conditional writes and optimistic locking

DynamoDB's superpower is conditional puts, so use them where the package allows. On save()/update() you can attach key conditions to get CAS semantics:

User::where('id', '=', 'u_123')
    ->where('version', '=', $currentVersion)
    ->update(['name' => $newName, 'version' => $currentVersion + 1]);

Wrap it in a retry loop — a ConditionalCheckFailedException from the SDK means "someone else won, reload and retry". This is the pattern to build idempotent job handlers and webhook consumers on.

Partial updates vs. full replacement

This is the single biggest day-to-day difference from SQL:

  • save() on an existing model performs a PutItem, which replaces the whole item (any attributes you didn't load are wiped).
  • The builder ->update([...]) performs UpdateItem and patches only the named attributes.

Rule of thumb: for "change one field" flows (views counters, status flips, last_login_at), use ->update() and never save(). For "I have the full document in hand", save() is fine.

Escaping to the AWS SDK

Anything the builder doesn't model — BatchWriteItem (25 items), BatchGetItem (100 keys), TransactWriteItems, TransactGetItems, TTL, streams, DescribeTable — goes through the Aws\DynamoDb\DynamoDbClient. Resolve it from the container (or from the model's connection) and keep the raw calls in small, dedicated service classes. Mixing raw client calls and Eloquent calls in the same controller is the usual route to inconsistent retries and inconsistent data shapes.

Model events and observers

Eloquent events (saving, saved, deleting) fire, so observers for audit fields, updated_at stamping, or cache invalidation do work. Just keep in mind that update() on the builder bypasses model events (same as in Eloquent/Query Builder), so don't rely on observers to run for every write.

Testing

Spin up DynamoDB Local per test suite, create tables once in setUpBeforeClass, and truncate/Scan+DeleteItem between tests. There's no RefreshDatabase equivalent for DynamoDB, and there is no schema to migrate — you're responsible for table lifecycle in both local and CI.

Gotchas and Tips

  • No migrations, ever. The package is a client, not a schema tool. Put table definitions in IaC. Adding a GSI in production is a real DynamoDB operation with its own backfill cost, not a config change.
  • where ≠ SQL WHERE. First condition must be equality on the hash key. Everything else is either a range condition on the sort key or a post-read filter. If you're writing orWhere across partition keys, you actually want multiple queries and a merge in PHP.
  • LIKE doesn't exist. begins_with is the only prefix primitive; there's no substring search. For real search, export to OpenSearch/Postgres.
  • Empty strings are rejected. DynamoDB disallows "" as an attribute value. Normalize incoming form data ($value === '' ? null : $value) or strip nulls before save; otherwise you'll get a ValidationException.
  • Numbers are strings under the hood. Store timestamps and counters with explicit $casts and don't lose precision. For IDs or money that exceed PHP int, keep them as strings and cast carefully — DynamoDB numbers have 38 digits of precision and nothing more.
  • find() requires the exact key. With a composite key, passing only the hash value silently errors. Use find([$hash, $range]) or query with where() on both keys.
  • paginate() totals are expensive. The paginator's total() is not a real count. Don't show "1,234 results" in the UI unless you maintain a counter. Same for ->count() on any filter-based query.
  • Item and response size limits. Items max 400 KB; Query/Scan pages max 1 MB. Pagination in the package is ExclusiveStartKey-based, so loop on LastEvaluatedKey for exports rather than assuming get() returned everything.
  • save() overwrites; update() patches. Covered above, but it's worth repeating because it's the bug you'll actually ship. A "partial" save() on a model with only id and one field set will wipe the rest of the item.
  • Throttling looks like random failures. Provisioned-capacity tables will throw ProvisionedThroughputExceededException, and GSIs throttle independently of the base table. Enable the SDK's built-in retry mode (retries / adaptive mode) in config/dynamodb.php, and consider on-demand capacity for spiky workloads.
  • GSI reads are eventually consistent. A "create then immediately query by email" flow will sometimes miss. If a read must be strongly consistent, query the base table by key, not a GSI.
  • Timestamps need a home. If $timestamps = true, created_at/updated_at are written as attributes — they must be declared in the table schema if you query or index on them, and updated_at only moves when Eloquent writes run (not on builder update()).
  • Watch the sort key type. $sortKeyType defaults are easy to get wrong; a numeric created_at with $sortKeyType = 'S' produces a ValidationException that's hard to read. Set 'S' or 'N' explicitly and keep it consistent with your writes.
  • Table prefixes and multiple connections. If you shard by environment, use the config's table prefix (or a separate connection) rather than string-concatenating the table name in every model.
  • Local DynamoDB is not production. It has no IAM, no real throttling, and a different consistency story. Run your conditional-write and pagination tests against it, but validate capacity and GSI behavior in a real sandbox account.
  • Keep a thin repository layer. This package is a good Eloquent-shaped facade, but its feature surface is deliberately narrow (no joins, no aggregations, no batch helpers). Wrapping access in UserRepository behind interface methods keeps the AWS SDK escape hatches out of your controllers and makes the eventual "move this table to Postgres" refactor mechanical rather than archaeological.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource