The Ampwall Team Blog

News, updates, and commentary from the people building Ampwall.

Happy (almost) New Year from Ampwall
Chris Grigg
Dec 30, 2024

Hello, friends! Continuing our tradition (?) of sending a message right before a holiday, we’re here for wish you Happy Holidays, Happy New Year, and thank you for being with us.

This is a word-heavy message. The tldr: Ampwall had a tremendous year of updates, growth, and friends. Our community is growing, our team is growing, and we are filled with confidence. Thank you for being a part of it; we couldn’t do it without your support. We hope you will continue to be with us throughout in 2025 and beyond. If you haven’t signed up yet, you can do so from https://ampwall.com.

Ampwall’s 2024 in review

In January 2024, I was the only person working full-time on Ampwall. There was exactly one user: me. There was exactly one band: mine. There were a handful of sales: to my friends, mostly. But that changed quickly.

We made a lot of friends

Ampwall’s invitation-only alpha test started in January and our first users joined. We spent the majority of the year continuing the alpha test and along the way something magical happened: bands and artists kept showing up and loving it. More contributors joined our team — I’m now one of twelve!

Our public beta test launch in September was our biggest news of the year, of course. Since then we’re up to almost 500 subscribers, more than 1800 users (!) in the system, more than 700 bands and artists, 1500 music releases, 9300 tracks, and 700 sales.

Our Discord, open to every subscriber and any user who’s spent as little as $1 (it’s an anti-spam thing) is taking off. We have more than 300 members with more joining all the time. Through our Discord, we get constant feedback, thoughts, and bug reports. But almost more importantly, people are making connections, getting encouragement, and feeling the support that comes with being part of a community.

We joined Bluesky. It’s fun! You can say hello to us here if you haven’t yet.

Ampwall artists are making real money. We’re seeing increased sales volume very month, with more music releases and especially exclusive merch drops.

A trend we saw early is continuing: buyers choose to cover Ampwall’s low transaction fee (5% + PayPal VS Bandcamp’s 10-15% + PayPal) a whopping 80% of the time, leaving only PayPal fees. Fans are taking note and recognizing that if you want to support artists directly, Ampwall is the best way to buy music and merch from bands online.

We did a lot of work

We had a huge year of platform updates. If anything, the thing we’ve done the worst job of is sharing the news about the great stuff we’ve built! Every single aspect of Ampwall was rebuilt and the overall breadth of the platform expanded dramatically. On a core level we now have…

  • A completely rebuilt UI for Artists, Music, and Merch. Ampwall today is completely unrecognizable compared to a year ago.

  • A new rich text editor to add links and text customization (sizes, bold, italic, etc,…) to item descriptions, bios, and blog posts.

  • A brand new navigation system that improves the experience for mobile, desktop, and especially users who manage multiple artists.

  • A new production infrastructure that will let us scale up, release faster, and keep our costs way down. Our monthly expenses are ~1/5 what they were at the start of the year.

  • Our mission manifesto that explained why we do this and how we see things differently acted as an AHA! moment for many people and helped share our vision.

  • Our content policy against LLM-generated content is an industry first. To my knowledge, Ampwall has the most artist-friendly policy against AI content of any platform. Read it here under “AI Content”.

If you’re releasing music and merch, you have more tools.

  • Music release preorders with fulfillment/release scheduling.

  • Free downloads with reusable links that can be set to expire after a fixed number of consumptions or time-based.

  • Pay-what-you-want downloads.

  • Scheduled music and merch release notifications, a PR-friendly approach that works around your schedule.

  • Exclusive audio embeds so you can give specific websites priority when premiering your work.

  • Bulk audio upload.

  • Showcase, our unique experience for announcing albums and sharing private links, perfect for pitching to labels.

  • Shipping price groups for EU sellers.

  • A new friendly Artist Page Setup experience.

  • Enhanced page analytics.

  • Users can be marked as members of bands. These links appear on artist pages and user profiles — a new avenue for organic discovery.

  • Supported Artists, our take on the MySpace Top 8, lets you create prominent links between your pages or your friends or just people you respect, another great tool for discovery.

  • Press links section on every music page so reviews, interviews, and articles don’t live and die by the whims of social media algorithms.

  • Mailing lists that don’t require subscribers to have Ampwall accounts.

We started work on tools for music communities — not just people making sounds but also the fans and supports that keep independent music alive.

  • Follow Artists and receive email notifications when they release music and merch. Blog post notifications coming soon!

  • Lists, our innovative and fun curation tool for music releases.

  • Search by artist name and tags, the start of our Exploration tools.

  • Discord integration. Our wonderful Discord community is now more than 300 people strong. It has a feed of new music additions that also tags members to help friends find each others’ work.

  • User profiles with an image and bios.

  • Lyrics on the audio player.

  • Share buttons all over the place with QR codes that you can save and print out.

  • Supporter comments on music releases.

  • Artwork credit links on every music and merch page.

We’re releasing major updates going to many of these areas in the coming weeks, too.

We have a lot of plans

We have ambitious goals for 2025.

We will continue to iterate on the experience of bands and artists, including new features for collaborative releases. We will add seats to the table, opening Ampwall up to crucial members of music communities who have so far been excluded from every other platform. We will release our first entries into features for labels.

We’re especially motivated to improve the individual fan/supporter and listener experience. In pursuit of that, we will invest heavily in tools for music discovery, curation, collaboration, and community connections.

Fundraising?

A tricky issue we still haven’t cracked is fundraising. Ampwall is still 100% funded by our team and our revenue from subscriptions and sales. The good news is our subscription strategy now generates enough revenue to cover our server and cloud costs! Subscriptions also help us keep spam and abuse off the platform.

We explored fundraising opportunities in 2024 and, frankly, things did not pan out. Our commitment to our mission, incorporation as a PBC, refusal to hitch our wagon to AI and crypto, and preference for steady sustainable growth does not make things easy. In 2025, we will continue seeking options to fundraise. If you or anyone you know is interested in helping us with this, please reach out to me via [email protected].

In the meantime, the best way to support our mission is to by signing up as a user from https://ampwall.com and creating a yearly subscription.

Ampwall will only improve

I am immensely proud of what we built so far. Even in our beta testing state, Ampwall is now the most complete and best competitor to Bandcamp in the world. We are also the only one of the emerging Bandcamp competitors that can sell merch, making us the only comprehensive alternative.

And we did it in two years with $0 outside funding. Can you imagine where we’ll be 12 months from now? Can you imagine 5 years?

Thank you

I am consistently awed by the outpouring of support we receive from our community. From the bottom of my heart, thank you so much for your encouragement, trust, and patience. I hope you’ve enjoyed our sporadic mailing list updates and, if you’re a user and/or in our Discord, we hope that we’ve made this complicated year a little brighter.

From everyone on the Ampwall Team, we hope you have a wonderful start to 2025. We’ll see and hear you soon.

Best,

Chris Grigg
[email protected]

Introducing our new Navigation System
Chris Grigg
Nov 27, 2024

In case you didn't notice, we completely rebuilt Ampwall's navigation system. It launched weeks ago and I simply forgot to post about it. Surprise! Or not a surprise since maybe you saw it.

We introduced the last version of our nav early in 2024. It used a vertical column along the left side of the page to present Artist Administration navigation. Other interactive elements relating to the user -- cart, user profile, search -- were at the top left corner, the idea being to keep dynamic interactive elements centered on the left side of the page. On mobile, the left-hand bar moved into a bottom-of-the-page menu and those same user controls went down there as well.

The last version of the nav was quite cool looking but feedback indicated that it needed work. Users reported it wasn't very discoverable since the buttons lacked text labels. Users with shorter screens (like 13" MacBook Air) reported icons were missing, which would have necessitated overflow logic. We also heard that having things like the user icon and shopping cart on the left side of the screen wasn't where most users expected them to be.

The different versions between desktop and mobile caused challenges for us in the code since were maintaining multiple versions of navigation. Having a bar on the left side of the page but only for logged-in Ampwall Subscribers required some challenging grid logic.

None of this was insurmountable, of course. We worked through a lot of challenges and we could have continued iterating on our nav! We always expect some amount of evolution of features, especially something ambitious like our nav design. But in this case, we decided to go in a different direction entirely.

Our new nav is a complete rebuild. It does a few things.

First, we moved Search, User Profile, and Cart to the top right-hand corner of the screen. There are other conditional items that appear there, like “Join us on Discord”.

Second, we move the Artist Administrative controls to a secondary nav bar that sits underneath the primary nav. This continues the approach of having a strong separation of User Stuff and Band Stuff — the two bars are essentially independent. But by using a single horizontal bar, it reads a bit more easily (especially thanks to the addition of labels next to icons) and it lives in a space where users are most used to seeing navigation elements.

Third, it simplifies the architecture of the entire page by removing the conditional grid logic that would force signed-in subscribers’ content to accept a margin of around 48px on the left side of the screen. The new page layout is substantially simpler from a code perspective thanks to this.

Fourth, pleasing both users and software engineers, our desktop and mobile views share 100% of the same components! You get a consistent navigation experience across all viewports. The Artist Admin controls collapse into a menu as they overflow à la GitHub, which lets us potentially add more options as needed. (We are trying to slim them down, tbh.)

Where the engineering is concerned, the new nav isn’t all that complicated. Ampwall is built with Next.js and our nav is a combination of server- and client-rendered content. The server feeds in data like whether the user is signed in, info about the user to populate menus, list of Artists (“Distros” in Ampwall data model parlance) to present, and then client components render the interactive bits like Menus. The secondary nav menu that creates an overflow menu uses simple screen measuring logic to determine overflow and then enables items in a menu. There’s a little jank on mobile that I want to investigate here but for the most part it’s working well. I went mildly YOLO on this and launched it without a feature flag, just one big cutover from old to new with a single deploy, because the effort to build a separate path with a flag would have extended build time in a way that I didn’t think was reasonable. Good decision? This time, yes, it all worked! But in general we do try to use feature flags for any substantial changes.

So that’s about it. I know this would be a lot more interesting with pictures. We’re going to work on that soon.

Want to talk about this more? Reach out on Ampwall’s Discord! We have a new #codewall channel specifically for our large community of artist + software engineers. You can also find me on bsky as @subvertallmedia.com.

New Feature - Exclusive embeds
Curt Howard
Oct 24, 2024

Throughout my career I’ve realized that few things are more difficult than writing your first feature in a new and unfamiliar code base. One of those things just so happens to be writing a blog post about it.

Hi. I’m Curt, one of the newest denizens of the Ampwall Kvlt. As part of, what seems to be, a eldritch startup hazing ritual, our great Gay Dracula has asked me to kick off Ampwall’s Engineering Blog with a post about my experience implementing a feature that I’m calling Exclusive Embeds.

The idea for this feature came directly from our wonderful community on Discord. We have a #thoughts-requests-complaints channel which we use to gather feedback from our subscribers. One such subscriber is Steve (of Am I In Trouble?, Negative Bliss, Ashenheart, and Tuffy) who is constantly giving us new ideas and feedback.

Steve came up with a composite idea which married our public embed code feature with Album Showcases. The Showcase feature allows an artist to create a dedicated page on Ampwall that provides an audio player that can bypass our rules around which tracks can be played during that album’s preorder period. The public embed code feature allows anyone on the internet to embed an audio player which does respect the preorder track rules into their website.

“Why not both?” I’m sure he thought as he suggested the idea to us on Discord.

Enter Exclusive Embeds

Exclusive Embeds allows an artist to set up a private embed code for their album that will render only on a single website. The audio player that the embed code renders completely bypasses all track streaming rules, thus granting the the website exclusive access to the album in its entirety. Steve wanted this feature because Sleeping Village Reviews approached him, asking if he would be open to them writing a review of the forthcoming Negative Bliss record, Everything Hurts and I’m Dying. We were happy to oblige.

In broad strokes the feature works like this: an Ampwall user logs into their account, either creates a new album or modifies an existing one, and finds a section called “Exclusive Embeds”. Clicking the Create button in that section brings up a modal which allows a user to type in one URL, something like https://blog.example.com. Once saved, we use that URL to determine the domain of the URL (like blog.example.com) and provide an embed code that will only work for embed requests coming from that domain. The user can modify the styling of that embed before copying the embed code and sharing it with the owner of the URL to use on their site.

For convenience, and for the love of chaos, we provide the ability to edit the URL after the embed has been created. We also allow users to delete them. In both cases, if the embed code is active on the internet, the player will stop rendering for that website. We warn you about the dangers of messing around with these embeds after they’ve been created.

A Proof of Concept

Saddle up, cowpokes. It’s about to get nerdy.

Our first inclination was to set a Content Security Policy header on incoming requests to the embed URL. It makes sense, right? I mean, that’s pretty much what frame-ancestors was created for.

The problem with this approach would be that we’d need to set the headers in middleware, and since this feature is backed by the database we’d also need to make a dreaded and generally ill-advised API request in middleware. Still, as a proof of concept, and because this was The Right Way™ to handle embed requests, I traipsed down this path for about two days. And, initially, the results were promising!

First, I created an sneaky, unpublished endpoint in our Next.js app that received the ID of the embed along with the hostname of the referer from the request. It was POST /api/albums/exclusive-embed (and it no longer exists, you salivating dogs, so good luck trying to access it). For a moment I thought about just sending the ID and having the server respond with the hostname, for ease of setting the CSP (ala frame-ancestors 'self' blog.example.com;) but that seemed overtly more risky than what I finally landed on:

import { NextRequest } from 'next/server';

import {
  PostAlbumExclusiveEmbedErrorResponseSchema,
  postAlbumExclusiveEmbedSchema,
  PostAlbumExclusiveEmbedSuccessResponseSchema,
} from './schema';

import { getCanAccessExclusiveEmbed } from './albumExclusiveEmbed.repository';

export const POST = async (req: NextRequest) => {
  const body = (await req.json()) as unknown;
  const parsedBody = postAlbumExclusiveEmbedSchema.safeParse(body);

  if (!parsedBody.success) {
    return Response.json(
      { status: 'error', message: parsedBody.error.message } satisfies PostAlbumExclusiveEmbedErrorResponseSchema,
      { status: 400 },
    );
  }

  const { embedId, refererHostname } = parsedBody.data;

  const hasAccess = await getCanAccessExclusiveEmbed({ id: embedId, allowedHostname: refererHostname });

  if (!hasAccess) {
    return Response.json(
      {
        status: 'error',
        message: 'You do not have access to this embed',
      } satisfies PostAlbumExclusiveEmbedErrorResponseSchema,
      { status: 403 },
    );
  }

  return Response.json({ status: 'success' } satisfies PostAlbumExclusiveEmbedSuccessResponseSchema, {
    status: 200,
  });
};

In this code we ensure that the body of the POST succeeds validation before querying the database for access and responding accordingly. Simple. A request will succeed or fail and will provide no other useful data.

Here’s the code that made use of this route:

import { NextRequest, NextResponse } from 'next/server';

import { PostAlbumExclusiveEmbedSchema } from './schema';

import { normalizeHostname } from './exclusiveEmbed.utils';

export async function middleware(request: NextRequest) {

  // Snip...

  if (request.nextUrl.pathname.startsWith('/services/PlayerCard/v1/content/exclusive')) {
    const embedId = request.nextUrl.searchParams.get('embedId');
    const referer = request.headers.get('referer');

    if (!embedId || !referer) {
      return new NextResponse('Forbidden', { status: 403 });
    }

    const refererHostname = normalizeHostname(referer);

    const fetchResponse = await fetch(`${request.nextUrl.origin}/api/album/exclusive-embed`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ embedId, refererHostname } satisfies PostAlbumExclusiveEmbedSchema),
    });

    const nextResponse = NextResponse.next();

    if (!fetchResponse.ok) {
      nextResponse.headers.set('Content-Security-Policy', "frame-ancestors 'none';");
    }

    return nextResponse;
  }

  return NextResponse.next();
}

In this code we’re using Nested Middleware to activate when someone is trying to access the exclusive embed audio player route, ensuring we have an ID and the referer (the latter meaning that this isn’t a direct request to the embed URL), and sending a normalized version of the hostname to the aforementioned API endpoint. Again, if that endpoint’s response is unsuccessful for any reason we disallow embed access via the CSP header.

Testing this was kind of fun, too, but I should also mention that I’m a bit of a glutton for punishment:

import { NextRequest } from 'next/server';

import { middleware } from './middleware';

describe('when handling exclusive embeds', () => {
  const embedUrlBase = 'https://ampwall.com/services/PlayerCard/v1/content/exclusive';

  it('should deny access if embedId is missing', async () => {
    const embedUrl = new URL(embedUrlBase);
    const request = new NextRequest(new Request(embedUrl));

    const response = await middleware(request);

    expect(response.status).toBe(403);
  });

  it('should deny access if referer hostname is missing', async () => {
    const embedUrl = new URL(`${embedUrlBase}?embedId=123`);
    const request = new NextRequest(new Request(embedUrl));

    const response = await middleware(request);

    expect(response.status).toBe(403);
  });

  it('should set Content-Security-Policy header if fetch response is not ok', async () => {
    const embedUrl = new URL(`${embedUrlBase}?embedId=123`);
    const request = new NextRequest(new Request(embedUrl));
    request.headers.set('referer', 'https://www.example.com');

    const fetchMock = vi.spyOn(global, 'fetch').mockResolvedValueOnce(new Response(null, { status: 500 }));

    const response = await middleware(request);

    expect(fetchMock).toHaveBeenCalledWith(`${request.nextUrl.origin}/api/album/exclusive-embed`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ embedId: '123', refererHostname: 'example.com' }),
    });

    expect(response.headers.get('Content-Security-Policy')).toBe("frame-ancestors 'none';");

    fetchMock.mockRestore();
  });
});

It’s a pretty straightforward test, but testing middleware in Next.js was new to me. You can see a bit of the hostname normalization we do here in the third case, where we have a referer of https://www.example.com but send example.com to the API route. We’re making the assumption that people saving a URL prefixed with a www. subdomain are just assuming that any request from that domain should be allowed.

Okay, but what’s wrong with this approach?

Well, a few things, actually. Aside from it being frowned upon as a poor design pattern, since the feature relied on the response from the server to set the CSP header we were forced to await the response from the API which increased latency for these requests. Generally speaking you would want middleware to resolve as fast as possible so the page can be rendered to the user sooner.

But the real problem lay in production. We launched the feature behind a feature flag so that I could make sure the positive results I was getting in my local environment were reproducible on the server and, of course, they weren’t. This is another reason why complex operations should be avoided in middleware; they can be really difficult to debug!

In production I was testing the embed codes through two separate domains I own, and both were exhibiting the same issue: on first page load the CSP header was being set, even though the domains matched perfectly. I’ll spare you the gory details of how we got to the bottom of the issue, but suffice it to say, with eyes rolling firmly to the back of my skull, it was a caching issue.

The code you see above is the result of all the debugging. I’m speaking specifically about the request.nextUrl.origin part. This used to be AMPWALL_URL, an environment variable representing our public URL. When using the environment variable this call was actually leaving our network before hitting our API, which meant that it was subject to passing back through Cloudflare, our caching service. Intermittently, Cloudflare would deny the request with a 403 seemingly stemming from a firewall setting. To get around this, and to keep the request internal, I used our server’s internal address, which was returned by request.nextUrl.origin but that also reported the wrong protocol, so our AWS instance would consistently block those requests.

Frustrated, I went back to the team with my findings, which is when we all realized that there was a better way…

The Right Way™ ain’t always best!

With Steve’s review date looming and a Gordian knot of cache bypass rules being proposed, we realized that we approached the problem wrong, albeit with the best of intentions. See, Next.js’ server components can handle this type of stuff pretty well on their own. You’re able to use next/headers to grab the referer pretty easily and, though you wouldn’t be able to set a response header that late in the game, you can still conditionally render whatever you want based on that header. So that’s what I did.

Here is but a piece of the component, showcasing the rendering logic:

const Exclusive = async ({ searchParams }: { searchParams?: ExclusiveSchema }) => {
  const headersList = headers();
  const referer = headersList.get('referer');

  if (!referer) {
    return <div>Not found</div>;
  }

  const parsedParams = exclusiveSchema.safeParse(searchParams);

  if (!parsedParams.success) {
    return <div>Not found</div>;
  }

  const { embedId, controlStyle, playerWidth } = parsedParams.data;

  const hasAccess = await getCanAccessExclusiveEmbed({ id: embedId, allowedHostname: normalizeHostname(referer) });

  if (!hasAccess) {
    return <div>Not found</div>;
  }

  // Snip...
};

First, if there is no referer set (meaning someone is trying the URL to gorge directly on its tender viddles), or if the params are invalid, or if there is a mismatch between the embed’s ID and the referer hostname, then render some text and bail out. Easy. Simple. What the heck.

But, still, we’d want to test this. At this point in my career I’m pretty comfortable testing React components using Testing Library but my experience has been entirely client side. I had to figure out how my knowledge might translate to server components. It took me a few stabs, but here are the tests:

import { render, screen } from '@testing-library/react';
import { randomUUID } from 'crypto';
import { headers } from 'next/headers';
import * as React from 'react';
import { initialize, resetSequence } from 'spec/__generated__/fabbrica';
import { expect, vi } from 'vitest';

import { getAlbumByExclusiveEmbedId } from './album.repository';
import { getCanAccessExclusiveEmbed } from './albumExclusiveEmbed.repository';

import Page from './page';

vi.mock('react', async (importOriginal: () => Promise<typeof import('react')>) => ({
  ...(await importOriginal()),
  cache: (fn: () => unknown) => fn,
}));

vi.mock('next/headers', async (importOriginal: () => Promise<typeof import('next/headers')>) => ({
  ...(await importOriginal()),
  headers: vi.fn(),
}));

vi.mock('@/repositories/albumExclusiveEmbed.repository', () => ({
  getCanAccessExclusiveEmbed: vi.fn(),
}));

vi.mock('@/repositories/album', () => ({
  getAlbumByExclusiveEmbedId: vi.fn(),
}));

const mockReferer = (referer: string | null) => {
  const headersMock = vi.mocked(headers);

  headersMock.mockImplementationOnce(
    (): ReturnType<typeof headersMock> => ({
      ...headersMock(),
      get: vi.fn(() => referer),
    }),
  );
};

describe('Exclusive Embed Page', () => {
  beforeEach(() => {
    initialize({ prisma: vPrisma.client });
  });

  beforeEach(() => resetSequence());

  it('prevents direct access', async () => {
    mockReferer(null);

    render(await Page({ searchParams: undefined }));

    expect(screen.getByText('Not found')).toBeDefined();
  });

  it('offers search param validation', async () => {
    mockReferer('https://example.com');

    render(await Page({ searchParams: { embedId: 'invalid', controlStyle: 'persistent', playerWidth: 350 } }));

    expect(screen.getByText('Not found')).toBeDefined();
  });

  it('protects against unwanted access', async () => {
    mockReferer('https://example.com');

    vi.mocked(getCanAccessExclusiveEmbed).mockResolvedValueOnce(false);

    render(await Page({ searchParams: { embedId: randomUUID(), controlStyle: 'persistent', playerWidth: 350 } }));

    expect(screen.getByText('Not found')).toBeDefined();
  });

  it('protects against invalid album', async () => {
    mockReferer('https://example.com');

    vi.mocked(getCanAccessExclusiveEmbed).mockResolvedValueOnce(true);
    vi.mocked(getAlbumByExclusiveEmbedId).mockResolvedValueOnce(null);

    render(await Page({ searchParams: { embedId: randomUUID(), controlStyle: 'persistent', playerWidth: 350 } }));

    expect(screen.getByText('Not found')).toBeDefined();
  });
});

I ran into a bunch of issues testing this. First was an error coming from the component declaring that cache wasn’t defined. I got around that by mocking the cache function to return whatever was passed to it:

vi.mock('react', async (importOriginal: () => Promise<typeof import('react')>) => ({
  ...(await importOriginal()),
  cache: (fn: () => unknown) => fn,
}));

Next, there was the task of mocking headers in TypeScript:

vi.mock('next/headers', async (importOriginal: () => Promise<typeof import('next/headers')>) => ({
  ...(await importOriginal()),
  headers: vi.fn(),
}));

// Snip...

const headersMock = vi.mocked(headers);

headersMock.mockImplementationOnce(
  (): ReturnType<typeof headersMock> => ({
    ...headersMock(),
    get: vi.fn(() => 'https://example.com'),
  }),
);

After that I had to figure out how to render the component, which I foolishly thought would be as easy as render(<Component />). But server components are async, so you’ve got to call them like the functions they truly are:

Lastly, we’re using vitest (which is great, btw) but I’m used to jest. In jest you have some additional matchers coming from Testing Library that allow you to assert expectations .toBeInTheDocument(). This was actually the first time we had unit tested a server component using JSDom, so there’s still a bit of plumbing to do to be able to use these convenient matchers in our code base. But, believe you me, when I figure that part out I’ll probably write another blog post about it :D

Thanks for reading!

I hope y’all enjoy the new feature and weren’t too bored with the walk-through. Let me know if you have any comments on approach or spot anything that I may have missed!

Curt “I’m Just Happy To Be Here” Howard
Engineering @ Ampwall

Hello, world!
Chris Grigg
Oct 20, 2024

Our team works fast. We release a lot of new features, bug fixes, and subtle tweaks to the platform every week. A lot (most?) of this work is the direct result of conversations with the community through Discord. This gives us feedback loops that we love:

Someone asks for something -> We chat about it -> We build it -> We let them know it's ready -> We get feedback on the result.

This is great! We think that our Discord community is one of the best parts of Ampwall and these tight feedback loops let us go from concept to deployment fast.

But... not everyone is in Discord. And some of the people in Discord aren't the always-online maniacs (I mean that in a nice way) that some of us are. So many of our updates go out with less fanfare than they deserve. Or maybe we learned something important while building and really want to share it with the world -- where should that go?

So here we are: the blog. In the spirit of eating our own dogfood, it's powered by the same code that every Ampwall artist uses to write their blogs, so our team will get more reminders about things that can be improved. We'll use this space to write about platform updates, company news, and anything that we think is fun or interesting.

You should still join us on Discord. ;-) Every Ampwall subscriber and anyone who's spent as little as $1 (so we know you're a real person) can get in. Just click the "Join us on Discord" button at the top of the page!

As always, reach me at [email protected] or hit up Ampwall general inbox through [email protected].