DEV Community

Cover image for CloudFront Signed URLs: Private Downloads with Lambda
Sascha Mirow-Stanitzok
Sascha Mirow-Stanitzok

Posted on Originally published at sascha-ms.de

CloudFront Signed URLs: Private Downloads with Lambda

CloudFront signed URLs are my answer to a question that shows up right after every upload feature: how do the files get back out safely? In my article on the S3 presigned URL for uploads, PDFs and photos land directly in the bucket. For downloads, many teams reach for an S3 presigned URL again. That works, but it has limits you only notice once users complain about expired links or the S3 bill for outbound traffic starts to grow.

In this article you build a private download for a fictional city library, step by step: anyone who has borrowed an e-book gets a short-lived, signed CloudFront link from a Lambda function. Everyone else gets a 403. This time I describe the infrastructure with the Serverless Framework and write the code in TypeScript, and we verify the signature with tests that run without any AWS account.

In short

With CloudFront signed URLs, your Lambda function signs download links with its own private key, and CloudFront checks them at the edge using the matching public key from a key group. Validity no longer depends on the lifetime of IAM sessions, and the bucket stays completely private. The most common pitfall: without a trusted key group on the cache behavior, CloudFront serves the files to anyone who knows the address.

What you will learn in this article

  • When CloudFront signed URLs fit better than S3 presigned URLs
  • How key groups, public keys and origin access control work together
  • A complete download endpoint with the Serverless Framework and TypeScript
  • How to test signatures locally and track down typical 403 errors

Why not just use an S3 presigned URL?

An S3 presigned URL is a regular S3 address with a signature in the query string. It is created with the AWS credentials of whoever issues it. That is exactly the catch with Lambda: your function runs with temporary credentials from its IAM role. According to the S3 documentation, a presigned URL stops working when that role session expires, even if you asked for a longer lifetime. With long-term keys of an IAM user you get at most seven days, but I don't want to see such keys in any Lambda function.

Two practical points come on top. First, every download is served straight from S3, with no caching close to the readers. Second, the bucket name appears in every URL. That is not a security problem, but it leaks internals and makes moving the files later harder. CloudFront signed URLs solve both: you sign with your own key pair, which has nothing to do with IAM, and the files come from the CloudFront edge cache.

Comparison table: S3 presigned URL vs. CloudFront signed URL by signing, validity, verification and caching

S3 presigned URL and CloudFront signed URL side by side

To be fair, the S3 option is often good enough. For an internal export that someone downloads five minutes after clicking, you don't need a CDN distribution. Once many people load the same files, links need to outlive a role session, or you already run CloudFront in front of your application, switching pays off.

How CloudFront signed URLs work

The principle is asymmetric cryptography. You create a key pair made of a private and a public key. You upload the public key to CloudFront and put it into a key group, which is simply a list of trusted keys. You attach this key group to a cache behavior of your distribution. From then on, CloudFront only accepts requests with a valid signature for that path.

Only your application knows the private key. The Lambda function uses it to build a so-called policy, a small JSON statement such as “this file, valid until 3:10 pm”, signs it and appends expiry, signature and key ID to the URL. CloudFront verifies the signature at the edge with the public key before anything is loaded from the cache or the origin.

AWS explicitly recommends key groups over the old CloudFront key pairs. Only the account's root user can manage those old key pairs in the console, while key groups can be managed via the API and infrastructure as code. According to the documentation, a key group holds up to five public keys by default, which is enough for a clean rotation. The key pair must be RSA 2048 or ECDSA 256, in PEM format.

Canned policy or custom policy?

CloudFront knows two kinds of policies. A canned policy only sets an expiry time and produces short URLs. A custom policy can also include a start time, IP ranges and wildcards in the path, so one signature covers several files. In return, the full policy travels base64-encoded in the URL, which makes it noticeably longer.

Feature Canned policy Custom policy
Expiry time yes yes
Start time no optional
Restrict IP range no optional
One signature for several files no yes, via wildcards
URL length short longer

For downloading a single file I use the canned policy. IP restrictions sound tempting, but in practice they often fail on mobile networks and corporate proxies, where the address changes between fetching the link and starting the download. If you want to unlock many files at once, say all pages of a comic, look at signed cookies instead. They follow the same principle, except the signature lives in a cookie rather than in the URL.

New since April 2026: SHA-256 instead of SHA-1

For a long time CloudFront only used SHA-1 for signatures. Since April 2026, CloudFront also supports SHA-256. To use it, you add the Hash-Algorithm=SHA256 parameter to the URL, or the CloudFront-Hash-Algorithm attribute for cookies. Existing links without this parameter keep using SHA-1. The AWS SDK for JavaScript already supports it: the @aws-sdk/cloudfront-signer package has an algorithm: "SHA256" option. For new projects I wouldn't configure anything else, especially since it costs nothing extra.

When CloudFront checks the expiry

One detail that often causes confusion: CloudFront checks the expiry at the time of the HTTP request. A large download that starts shortly before expiry will finish. If the connection drops and the client retries after expiry, it gets a 403. The same applies to range requests, which video players or PDF viewers use to load files piece by piece. So don't make the validity too tight. Ten minutes is a reasonable starting point for an e-book.

The example: private e-book downloads for a library

Our fictional city library lends e-books as PDFs. The files live in a private S3 bucket that only CloudFront may access. Readers sign in through an identity provider and send a JWT, a signed login token, to an HTTP API. The Lambda function checks in DynamoDB whether the book is currently on loan, loads the private key from SSM Parameter Store and returns a signed URL. The browser then downloads the PDF directly from CloudFront.

🎬 Flow of a signed download – this animated diagram only runs on my site: watch it on sascha-ms.de

The Lambda function never touches the file itself. It only decides whether someone may get it. That is the same pattern as uploads with presigned URLs, just in the other direction, and it keeps the function small, fast and cheap.

Step by step: CloudFront signed URLs with Lambda

Prerequisites

  • An AWS account with permissions for CloudFront, S3, DynamoDB, SSM, IAM and API Gateway
  • Node.js 24 and Serverless Framework v4 (npm i -g serverless)
  • OpenSSL and AWS CLI v2
  • An identity provider that issues JWTs, for example Amazon Cognito (you will need the issuer URL and audience in a moment)
  • Basic knowledge of TypeScript and HTTP APIs

Step 1: Create and store the key pair

First you need the key pair. You store the private key as a SecureString in Parameter Store and the public key as a plain string. The Serverless Framework reads the public key from Parameter Store during deployment and passes it on to CloudFront.

openssl genrsa -out private_key.pem 2048
openssl rsa -pubout -in private_key.pem -out public_key.pem

aws ssm put-parameter --name /library/dev/cloudfront/private-key \
  --type SecureString --value file://private_key.pem
aws ssm put-parameter --name /library/dev/cloudfront/public-key \
  --type String --value file://public_key.pem

rm private_key.pem
Enter fullscreen mode Exit fullscreen mode

I mean that last command. The private key does not belong in the repository, in an environment variable or on your laptop. Whoever holds it can issue links for every file behind this distribution.

Step 2: The infrastructure in serverless.yml

The serverless.yml contains the function, the HTTP API with a JWT authorizer and, as CloudFormation resources, the bucket, table, public key, key group, origin access control and distribution. Origin access control (OAC) makes CloudFront authenticate to S3 with SigV4, so the bucket needs no public access at all.

# serverless.yml
service: library-downloads
frameworkVersion: "4"

provider:
  name: aws
  runtime: nodejs24.x
  region: eu-central-1
  memorySize: 256
  timeout: 10
  httpApi:
    authorizers:
      readerJwt:
        type: jwt
        identitySource: $request.header.Authorization
        issuerUrl: ${param:jwtIssuerUrl}
        audience:
          - ${param:jwtAudience}
  environment:
    LOANS_TABLE: !Ref LoansTable
    DOWNLOAD_DOMAIN: !GetAtt DownloadDistribution.DomainName
    KEY_PAIR_ID: !Ref ReaderPublicKey
    PRIVATE_KEY_PARAM: /library/${sls:stage}/cloudfront/private-key
  iam:
    role:
      statements:
        - Effect: Allow
          Action: dynamodb:GetItem
          Resource: !GetAtt LoansTable.Arn
        - Effect: Allow
          Action: ssm:GetParameter
          Resource: arn:aws:ssm:${aws:region}:${aws:accountId}:parameter/library/${sls:stage}/cloudfront/private-key

functions:
  signDownload:
    handler: src/handler.handler
    events:
      - httpApi:
          method: GET
          path: /books/{bookId}/download
          authorizer:
            name: readerJwt

resources:
  Resources:
    BooksBucket:
      Type: AWS::S3::Bucket
      Properties:
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true

    LoansTable:
      Type: AWS::DynamoDB::Table
      Properties:
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - { AttributeName: readerId, AttributeType: S }
          - { AttributeName: bookId, AttributeType: S }
        KeySchema:
          - { AttributeName: readerId, KeyType: HASH }
          - { AttributeName: bookId, KeyType: RANGE }

    ReaderPublicKey:
      Type: AWS::CloudFront::PublicKey
      Properties:
        PublicKeyConfig:
          Name: library-${sls:stage}-reader-key-1
          CallerReference: library-${sls:stage}-reader-key-1
          EncodedKey: ${ssm:/library/${sls:stage}/cloudfront/public-key}

    ReaderKeyGroup:
      Type: AWS::CloudFront::KeyGroup
      Properties:
        KeyGroupConfig:
          Name: library-${sls:stage}-readers
          Items:
            - !Ref ReaderPublicKey

    BooksOriginAccess:
      Type: AWS::CloudFront::OriginAccessControl
      Properties:
        OriginAccessControlConfig:
          Name: library-${sls:stage}-books-oac
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4

    DownloadDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          Comment: Signed e-book downloads
          Origins:
            - Id: books
              DomainName: !GetAtt BooksBucket.RegionalDomainName
              OriginAccessControlId: !GetAtt BooksOriginAccess.Id
              S3OriginConfig:
                OriginAccessIdentity: ""
          DefaultCacheBehavior:
            TargetOriginId: books
            ViewerProtocolPolicy: https-only
            CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # Managed-CachingOptimized
            TrustedKeyGroups:
              - !Ref ReaderKeyGroup

    BooksBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref BooksBucket
        PolicyDocument:
          Statement:
            - Effect: Allow
              Principal:
                Service: cloudfront.amazonaws.com
              Action: s3:GetObject
              Resource: !Join ["", [!GetAtt BooksBucket.Arn, "/*"]]
              Condition:
                StringEquals:
                  AWS:SourceArn: !Join ["", ["arn:aws:cloudfront::${aws:accountId}:distribution/", !Ref DownloadDistribution]]

  Outputs:
    BooksBucketName:
      Value: !Ref BooksBucket
    DownloadDomain:
      Value: !GetAtt DownloadDistribution.DomainName
    LoansTableName:
      Value: !Ref LoansTable
Enter fullscreen mode Exit fullscreen mode

Three places deserve a second look. TrustedKeyGroups on the default cache behavior is what turns on signature checks in the first place. The bucket policy allows s3:GetObject only for the CloudFront service and only for this exact distribution, checked via AWS:SourceArn. And in serverless.yml I use !Join instead of !Sub, because the Serverless Framework would try to resolve the ${…} syntax as its own variable. Almost everyone falls into that trap once.

Step 3: Sign the URL

The actual signing lives in a small, pure function without AWS calls. That makes it easy to test. It caps validity at one hour, encodes the path properly and uses SHA-256.

// src/sign-download.ts
import { getSignedUrl } from "@aws-sdk/cloudfront-signer";

export interface SignDownloadInput {
  domain: string; // e.g. d111111abcdef8.cloudfront.net
  objectKey: string; // e.g. books/the-little-baker.pdf
  keyPairId: string; // ID of the CloudFront public key
  privateKey: string; // PEM, never written to logs
  validForSeconds: number;
  now?: Date;
}

export interface SignedDownload {
  url: string;
  expiresAt: string;
}

const MAX_VALIDITY_SECONDS = 60 * 60; // never hand out links longer than one hour

export function signDownload(input: SignDownloadInput): SignedDownload {
  if (input.validForSeconds < 30 || input.validForSeconds > MAX_VALIDITY_SECONDS) {
    throw new Error(`validForSeconds must be between 30 and ${MAX_VALIDITY_SECONDS}`);
  }
  const now = input.now ?? new Date();
  const expires = new Date(now.getTime() + input.validForSeconds * 1000);
  const path = input.objectKey.split("/").map(encodeURIComponent).join("/");

  const url = getSignedUrl({
    url: `https://${input.domain}/${path}`,
    keyPairId: input.keyPairId,
    privateKey: input.privateKey,
    dateLessThan: expires,
    algorithm: "SHA256",
  });

  return { url, expiresAt: expires.toISOString() };
}
Enter fullscreen mode Exit fullscreen mode

Applying encodeURIComponent per path segment matters. CloudFront compares the signed resource with the URL that is actually requested. If the policy contains a space and the browser sends %20, the signature no longer matches.

Step 4: Load the private key

I fetch the key from Parameter Store on the first call and keep it in the execution environment's memory for five minutes. That saves calls and latency. At the same time, a key rotation takes effect within five minutes without redeploying the function.

// src/private-key.ts
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";

const ssm = new SSMClient({});
let cached: { value: string; loadedAt: number } | undefined;
const TTL_MS = 5 * 60 * 1000; // reload after 5 minutes so key rotation takes effect

export async function loadPrivateKey(parameterName: string): Promise<string> {
  if (cached && Date.now() - cached.loadedAt < TTL_MS) return cached.value;
  const res = await ssm.send(
    new GetParameterCommand({ Name: parameterName, WithDecryption: true }),
  );
  const value = res.Parameter?.Value;
  if (!value) throw new Error(`Parameter ${parameterName} is empty`);
  cached = { value, loadedAt: Date.now() };
  return value;
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The Lambda handler

The handler reads the user ID from the JWT claims that API Gateway has already validated. Then it checks the loan in DynamoDB and, if everything is fine, returns the URL and expiry time. I deliberately don't log the signed URL itself, because until it expires it works like an access key.

// src/handler.ts
import type { APIGatewayProxyEventV2WithJWTAuthorizer, APIGatewayProxyResultV2 } from "aws-lambda";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
import { signDownload } from "./sign-download";
import { loadPrivateKey } from "./private-key";

const db = DynamoDBDocumentClient.from(new DynamoDBClient({}));
const BOOK_ID = /^[a-z0-9-]{3,64}$/;

const json = (statusCode: number, body: unknown): APIGatewayProxyResultV2 => ({
  statusCode,
  headers: { "content-type": "application/json", "cache-control": "no-store" },
  body: JSON.stringify(body),
});

export const handler = async (
  event: APIGatewayProxyEventV2WithJWTAuthorizer,
): Promise<APIGatewayProxyResultV2> => {
  const readerId = event.requestContext.authorizer.jwt.claims.sub as string | undefined;
  const bookId = event.pathParameters?.bookId ?? "";
  if (!readerId) return json(401, { message: "Not signed in" });
  if (!BOOK_ID.test(bookId)) return json(400, { message: "Invalid book ID" });

  // Is the book currently on loan to this reader?
  const loan = await db.send(
    new GetCommand({ TableName: process.env.LOANS_TABLE, Key: { readerId, bookId } }),
  );
  const dueAt = loan.Item?.dueAt as string | undefined;
  if (!dueAt || new Date(dueAt) < new Date()) {
    return json(403, { message: "No active loan for this book" });
  }

  const privateKey = await loadPrivateKey(process.env.PRIVATE_KEY_PARAM!);
  const download = signDownload({
    domain: process.env.DOWNLOAD_DOMAIN!,
    objectKey: `books/${bookId}.pdf`,
    keyPairId: process.env.KEY_PAIR_ID!,
    privateKey,
    validForSeconds: 10 * 60,
  });

  console.log(JSON.stringify({ msg: "download signed", bookId, expiresAt: download.expiresAt }));
  return json(200, download);
};
Enter fullscreen mode Exit fullscreen mode

Step 6: Test locally without an AWS account

The test generates its own RSA key pair at runtime, signs a URL and verifies the signature against the canned policy using built-in Node.js functions. That way you test exactly what CloudFront does at the edge, just without a network.

// test/sign-download.test.ts
import { generateKeyPairSync, verify } from "node:crypto";
import { describe, expect, it } from "vitest";
import { signDownload } from "../src/sign-download";

const { privateKey, publicKey } = generateKeyPairSync("rsa", {
  modulusLength: 2048,
  privateKeyEncoding: { type: "pkcs1", format: "pem" },
  publicKeyEncoding: { type: "spki", format: "pem" },
});

const base = {
  domain: "d111111abcdef8.cloudfront.net",
  objectKey: "books/the-little-baker.pdf",
  keyPairId: "K2EXAMPLEKEYID",
  privateKey,
  validForSeconds: 600,
  now: new Date("2026-11-12T15:00:00Z"),
};

// CloudFront uses URL-safe base64: + becomes -, = becomes _, / becomes ~
const fromCloudFrontBase64 = (s: string) =>
  Buffer.from(s.replace(/-/g, "+").replace(/_/g, "=").replace(/~/g, "/"), "base64");

describe("signDownload", () => {
  it("creates a URL with expiry, key ID and SHA-256 marker", () => {
    const { url, expiresAt } = signDownload(base);
    const u = new URL(url);
    expect(u.hostname).toBe(base.domain);
    expect(u.pathname).toBe("/books/the-little-baker.pdf");
    expect(u.searchParams.get("Expires")).toBe(String(Date.parse(expiresAt) / 1000));
    expect(u.searchParams.get("Key-Pair-Id")).toBe("K2EXAMPLEKEYID");
    expect(u.searchParams.get("Hash-Algorithm")).toBe("SHA256");
    expect(expiresAt).toBe("2026-11-12T15:10:00.000Z");
  });

  it("produces a signature that the public key verifies (canned policy)", () => {
    const { url } = signDownload(base);
    const u = new URL(url);
    const expires = Number(u.searchParams.get("Expires"));
    const resource = `https://${base.domain}${u.pathname}`;
    const policy = JSON.stringify({
      Statement: [{ Resource: resource, Condition: { DateLessThan: { "AWS:EpochTime": expires } } }],
    });
    const signature = fromCloudFrontBase64(u.searchParams.get("Signature")!);
    expect(verify("sha256", Buffer.from(policy), publicKey, signature)).toBe(true);
  });

  it("rejects links that would live too long", () => {
    expect(() => signDownload({ ...base, validForSeconds: 24 * 3600 })).toThrow(/between 30/);
  });

  it("encodes special characters in the object key", () => {
    const { url } = signDownload({ ...base, objectKey: "books/coffee & cake.pdf" });
    expect(new URL(url).pathname).toBe("/books/coffee%20%26%20cake.pdf");
  });
});
Enter fullscreen mode Exit fullscreen mode
npm install
npx tsc --noEmit
npx vitest run
Enter fullscreen mode Exit fullscreen mode

Expected result: Tests 4 passed (4). I checked the code with Node.js, TypeScript 7.0 and Vitest 5.1, and additionally ran the resources from serverless.yml through cfn-lint.

Step 7: Deploy and try it out

serverless deploy --stage dev \
  --param="jwtIssuerUrl=https://cognito-idp.eu-central-1.amazonaws.com/<pool-id>" \
  --param="jwtAudience=<app-client-id>"

aws s3 cp the-little-baker.pdf s3://<BooksBucketName>/books/the-little-baker.pdf

aws dynamodb put-item --table-name <LoansTableName> --item \
  '{"readerId":{"S":"<sub-from-the-token>"},"bookId":{"S":"the-little-baker"},"dueAt":{"S":"2026-12-01T00:00:00Z"}}'

curl -H "Authorization: Bearer <jwt>" \
  https://<api-id>.execute-api.eu-central-1.amazonaws.com/books/the-little-baker/download
Enter fullscreen mode Exit fullscreen mode

The response contains a URL like https://dxxxx.cloudfront.net/books/the-little-baker.pdf?Expires=…&Key-Pair-Id=…&Signature=…&Hash-Algorithm=SHA256. Open it in the browser and the PDF loads. Change one character in the signature or request the address without parameters: CloudFront answers with 403. After deployment, serverless info --verbose shows the bucket and table names. A new distribution also needs a few minutes until it is active everywhere.

Common mistakes with CloudFront signed URLs

Files are reachable without a signature

This is the most dangerous mistake because nobody notices it. If TrustedKeyGroups is missing on the matching cache behavior, CloudFront simply ignores the signature and serves every file whose path someone knows. So after each deployment, check that an unsigned URL really gets a 403. If you have several behaviors, such as /books/* and /covers/*, check each one.

403 despite a fresh signature

The usual causes: the key group ID was passed as keyPairId instead of the public key ID. The private key belongs to a different public key than the one in the key group. Or the URL was changed after signing, for example by an extra parameter like ?download=1 or a different encoding of special characters. With a canned policy, the requested URL must match the signed one exactly.

403 from S3 instead of CloudFront

If the error comes back as XML with AccessDenied from S3, the signature is fine, but CloudFront is not allowed into the bucket. Check the bucket policy and whether the distribution ID in AWS:SourceArn is correct. If the file is encrypted with your own KMS key, CloudFront also needs permissions on that key.

Security, rotation and cost

Treat signed URLs like passwords with an expiry date. They don't belong in logs, analytics tools or emails that someone forwards weeks later. Short lifetimes and a fresh link per click beat a long-lived link issued in advance.

To rotate, add a second public key to the same key group, switch the function to the new private key and remove the old public key only after all links signed with it have expired. Since the function reloads the key every five minutes, the switch only needs an update in Parameter Store plus the new key ID in the configuration.

On the cost side, you mainly pay CloudFront for data transfer and requests, plus Lambda, API Gateway and DynamoDB for fetching the link. With the managed CachingOptimized cache policy, query strings are not part of the cache key. So all readers share the cached copy of a book, even though each of them has their own signature. You will find current prices on the CloudFront pricing page in the sources. For very large files, also have a look at my article on S3 multipart upload, because what gets uploaded big usually gets downloaded big as well.

My take

I consider CloudFront signed URLs the better default tool as soon as files go to end users. The split between “who may” (Lambda) and “who delivers” (CloudFront) is clean, and validity no longer depends on details like the session duration of an IAM role. With SHA-256 now available, compliance teams also lose a common objection.

I disagree with the widespread idea that this only pays off with heavy traffic. The extra effort is essentially four additional resources in serverless.yml. The real work is key management, and you should solve that properly anyway. If you only send an internal report now and then, though, the S3 presigned URL keeps things simpler, and that is perfectly fine.

FAQ

What is the difference between a CloudFront signed URL and an S3 presigned URL?

An S3 presigned URL is signed with AWS credentials and checked directly by S3. A CloudFront signed URL is signed with your own key pair and checked by CloudFront at the edge. It is independent of IAM sessions and benefits from the CDN cache.

How long can a CloudFront signed URL be valid?

You set the expiry yourself in the policy. CloudFront expects it as Unix time in seconds. Short lifetimes from minutes to a few hours make sense, so a shared link quickly becomes worthless.

Do I need the root user for CloudFront signed URLs?

No. With key groups you manage public keys via the API, CloudFormation or the Serverless Framework. Only the old CloudFront key pairs require the root user, and AWS advises against them.

Signed URLs or signed cookies?

Signed URLs suit single files and clients without cookies, such as apps or downloads via a link. Signed cookies work well when a user should fetch many files of one area without the URLs changing. If you use both, the signed URL takes precedence.

Your next step

Pick one place in your application that hands out an S3 presigned URL for downloads today and check two things: how long should the link really be valid, and which credentials sign it? If the answers don't fit together, that is a good candidate for CloudFront signed URLs. And I'm curious: do you already use CloudFront for private downloads, or is the S3 presigned URL enough for you? Let me know in the comments.

All code from this article, ready to run: smirowstanitzok/sascha-ms-examples

Sources

This post was created with AI assistance.


👉 Next step: Not sure which AWS service fits your use case? My AWS service finder asks a few questions and points you to the right one.

📬 Stay updated: New articles on serverless, AI and leading tech teams via RSS, browser push or the weekly newsletter (in German).

Originally published on sascha-ms.de. More articles on serverless, AI in everyday development and leading tech teams: sascha-ms.de/en

Top comments (0)