A micro-app for DIY digital product sales.
This project is a smallish Padrino web application. It has plenty of configuration options, and it's pretty approachable for plain old hack-it-to-be-what-you-want. Flexible digital product sales where you only pay for exactly the amount of storage and bandwidth you require.
My motivation for writing this is the front-loaded cost of the hosted solutions to selling digital products (such as FetchApp). This is not to say these apps are very expensive, or that they aren't worth the price, just that I wanted a DIY option.
Let's say I want to try selling some screencasts, and say I have 5GB of files to sell over the first few months (multiple versions of HD screencasts), I have to commit to spending $50/month (using FetchApp's pricing as an example) just to have them available for purchase.
Here's a comparison, price-wise, in some ficticious 1 month periods. Here I'll assume that each purchase is $10, and each sale results in 1GB of downloads.
So as you can see, the download volume is what really matters. With S3 and most other DIY cloud storage the amount you store is basically insignificant compared to how much downloading you do. Also it seems to me that, at $50k in sales, you are hardly going to care about the difference between spending $300 or $600 on your bandwidth charges.
In the first few months selling your new screencasts you could easily be losing money with FetchApp. Also it's worth pointing out that FetchApp is probably not making any money if you're serving 5TB of downloads at $300/month.
There is a clear trade-off here. You have to run the thing yourself. You set it up on Heroku or run it on your own VPS (I already have one, so this is easy) and you have to manage it yourself, make sure it doesn't go down or that there's an issue with payments going through/etc.
Obviously many people will simply opt to outsource that stuff to FetchApp and that's great. But if you want to do it yourself, you can use Sellbot!
How It Works
Currently the idea is that you have your product pages separate, in whatever
sort of application you are already using (e.g. static files, a wordpress
blog, craigslist posts?, whatever). You then write a
config.yml and a
store.yml (examples provided) and deploy the app somewhere - possibly
purchase.yourdomain.com makes sense here - and you're done! Sellbot can:
- collect an email address from the user
- take payment via a few different sources
- provide secure, time- and count- limited download links
- permalink to the user's order
send an email receipt(not yet)
- show you reports and
graphs(not yet) in an admin panel
- be configured or simply just edited to your preferences
store.yml contains information about your store, what products and 'packages'
are available, how much they cost, and so on. Here's a quick example:
products: screencast1: name: "A Screencast" subtitle: "A short description!" price: 9 files: high: screencast1-high.mov low: screencast1-low.mov code: screencast1-code.zip screencast2: name: "Another Screencast" subtitle: "It's the best screencast ever." price: 9 files: high: screencast2-high.mov low: screencast2-low.mov packages: all-screencasts: name: "All Screencasts" price: 15 contents: - screencast1 - screencast2
If you visit
http://example.com/all-screencasts you'll get an entry page
that explains what you're buying, how much it costs and asks for their
email address. Like so:
After this, depending on your configuration, the flow will be slightly different (they may have to go through Paypal's horrendous checkout process, etc) but eventually they will end up on a thanks page with download links.
Download links are derived from the value of the
files key and
file_key_map key in
config.yml. It should be obvious how it works
from this example:
file_key_map: high: "High Quality MP4" low: "Low Quality MP4" code: "Source Code"
So assuming the store example above, and a user having bought 'screencast1' they will see this:
You can configure how many downloads the user is allowed for each file, and the links on the page expire in 5 minutes detering people from just emailing them around. Or, you can just disable all that and go with the honor sytem :)
You'll need to write a fairly thorough
config.yml. There is an example
in the source (
config/config.yml.sample) you can use to start with.
Let's jump in... You'll want to define some stuff the same for both development
and production modes.
common: &common title: "QuickCasts Checkout" -- site title email_optional: false -- require an email address max_downloads: 5 -- omit for unlimited logo: "/img/logo.png" price: unit: '$' precision: 0 file_key_map: -- as above in store config ... support: "email@example.com"
Next we have a block for the
development environment. Likely you'll have
a similar one for production, of course.
development: <<: *common home: 'http://example.dev' host: 'http://purchase.example.dev' session: "SECRET" payment: ... db: ... storage: ... mail: ... admin: username: password
First we include the
common block. Then we define what the originating
site is (this is where you've presumably listed your products and links
into this app). The hostname of this purchase app itself, so it can write
permalinks to orders/etc. A logo image (you could just replace the sellbot
/img/logo.png and make the logo url just a path, too). A session
SecureRandom.base64(256) or similar?). At the end
we have N username/password combinations for access to admin. Why is this
in plain text? Because you don't have this file in version control, and
other stuff like AWS keys are much more valuable and scary anyway!
The stuff in the middle,
Putting AWS keys and so on in a yml file in your repository might not
sound like a good idea to you. In that case you can use
env.sample to write
a set of environment variables that you can do the right thing with.
For example Heroku's heroku-config
plugin makes this approach a good one. You'll find
symlinks are already in place if you write
Right now Sellbot 'officially' supports Stripe and Paypal as payment processors. Configuration for Paypal is significantly more complex and cumbersome than Stripe, but of course Stripe can't be used outside of the US :/
Stripe is easy because all you really need is an account. Simply login and find the settings modal via the 'Your Account' menu. On the 'Api Keys' tab you'll find both your test and production keys. You can use them like so:
payment: processor: Stripe config: publishable: ABC secret: XYZ
Uncomment the Stripe gem in
Gemfile and run
Oh boy this is a tricky one. Wouldn't it be great if Stripe was supported outside the US? Do yourself a favor and check right now before bothering with this PayPal junk, I'll wait...
Ok, so sorry to see that you have to go the PayPal route. It's a long one :(
These instructions are for making a sandbox account so you can test that this thing works. If you want to skip that part you can follow similar steps to do this for your real seller account.
Uncomment Nestful in
Go to developer.paypal.com and sign up for a new account.
Confirm the account in the email they send.
Create a 'preconfigured account' for both the seller and a fictitious 'buyer'.
On the 'Test Accounts' page you'll see your new users. Copy the email of the one which will be your seller, this is your
Decide if you're using the
ENVapproach or simply
mkdir -p etc/certs; cd etc/certs
env.samplefor how to define this stuff. It's very whitespace sensitive.
Generate a private key:
openssl genrsa -out app_key.pem 1024
Generate a public certificate:
openssl req -new -key app_key.pem -x509 -days 365 -out app_cert.pem
Note: If you don't specify
-daysthe default appears to be something like 1 month. You can specify huge numbers in the thousands, I will leave this as an exercise to the reader to choose an appropriate length of time.
From the top bar choose 'Profile' and then 'Encrypted Payment Settings'.
Add the public certificate (
app_cert.pem) we just created.
Take note of the 'Cert ID' afterwards, this becomes the
Just above the area where you upload your cert you will find a download link for PayPal's public cert. Put this along side your other certs, but rename it to
Back on the 'Profile' page choose 'Website Payment Preferences'.
Turn on 'Auto Return' and 'Payment Data Transfer'.
You will also need to fill out the 'Return URL' field even though we will obviously be overriding it with each request...
Enable 'Block Non-encrypted Website Payment'.
Now save, and if you did that bit correctly, the resulting page should have a yellow box on it that says, in teeny letters, 'please use the follow identity token'... Copy that because it will become the
Back on 'Profile', choose 'Instant Payment Notification Preferences'.
Again we will ender a bogus URL here (like just the root, for example) and enable the option, each request will override the URL specified.
If you're doing the sandbox thing you will need wait for PayPal to redirect you back to the
/complete/:order_idurl in order for the order to be marked as completed. Alternatively you can find the 'IPN History' page on PayPal, copy the parameters and the notify url and spoof the request yourself. The IPN handler actually doesn't rely on any parameters other than the order id in the URL and
txn_id. It then uses a PayPal API to check the status of the transaction authoritatively. So you don't need to worry about someone spoofing a successful transaction IPN in production.
Once that's all done, and the certs in place your config might look like:
payment: processor: PayPal config: cert_id: '...' identity_token: '...' business_id: '...' currency_code: 'CAD'
There is also a 'free' option which is really just a stub to bypass any checkout flow. Just specify 'Free' as the processor:
payment: processor: Free
Sellbot's data layer is pretty simple: storing orders, email addresses, payment processor responses and download counts.
- Full support for Redis
- Experimental DynamoDB adapter
- Working on more generic MongoDB and Sequel adapters
config.yml you simply need to specify your options like so:
db: adapter: Redis config: uri: http://127.0.0.1:6379 namespace: a-namespace
and you're done!
Note The DynamoDB adapter sort of 'technically' works, but it's a bit odd and not exactly production ready. Experiment. Contribute? :)
The section in
config.yml looks like so:
db: adapter: DynamoDB config: table: table-name read: 10 write: 5
Where read/write refer to the same parameters described in the DynamoDB FAQs
Not Yet Implemented
Not Yet Implemented
Right now only S3 is supported because it has easy support for secure/expiring links and is super cheap and convenient. Planned is also just a generic permalink style storage option (e.g. host and serve the files yourself) where you'd lose some of the features like download restrictions.
Maybe there are some other good options like CDN networks or CloudFiles?
All you need here is the bucket your files live in and your AWS credentials:
storage: provider: S3 config: access_key_id: "ACCESS" secret_access_key: "SECRET" bucket: bucket-name
Not Yet Implemented